tanjun

A flexible command framework designed to extend Hikari.

Examples

A Tanjun client can be quickly initialised from a Hikari gateway bot through tanjun.Client.from_gateway_bot, this enables both slash (interaction) and message command execution:

bot = hikari.GatewayBot("BOT_TOKEN")

# As a note, unless event_managed=False is passed here then this client
# will be managed based on gateway startup and stopping events.
# mention_prefix=True instructs the client to also set mention prefixes on the
# first startup.
client = tanjun.Client.from_gateway_bot(bot, declare_global_commands=True, mention_prefix=True)

component = tanjun.Component()
client.add_component(component)

# Declare a message command with some basic parser logic.
@component.with_command
@tanjun.with_greedy_argument("name", default="World")
@tanjun.as_message_command("test")
async def test_command(ctx: tanjun.abc.Context, name: str) -> None:
    await ctx.respond(f"Hello, {name}!")

# Declare a ping slash command
@component.with_command
@tanjun.with_user_slash_option("user", "The user facing command option's description", default=None)
@tanjun.as_slash_command("hello", "The command's user facing description")
async def hello(ctx: tanjun.abc.Context, user: typing.Optional[hikari.User]) -> None:
    user = user or ctx.author
    await ctx.respond(f"Hello, {user}!")

Alternatively, the client can also be built from a RESTBot but this will only enable slash (interaction) command execution:

bot = hikari.RESTBot("BOT_TOKEN", "Bot")

# declare_global_commands=True instructs the client to set the global commands
# for the relevant bot on first startup (this will replace any previously
# declared commands).
client = tanjun.Client.from_rest_bot(bot, declare_global_commands=True)

# This will load components from modules based on loader functions.
# For more information on this see `tanjun.as_loader`.
client.load_modules("module.paths")

# Note, unlike a gateway bound bot, the rest bot will not automatically start
# itself due to the lack of Hikari lifetime events in this environment and
# will have to be started after the Hikari client.
async def main() -> None:
    await bot.start()
    async with client.open():
        await bot.join()

For more extensive examples see the repository's examples.

View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""A flexible command framework designed to extend Hikari.

Examples
--------
A Tanjun client can be quickly initialised from a Hikari gateway bot through
`tanjun.Client.from_gateway_bot`, this enables both slash (interaction) and message
command execution:

```py
bot = hikari.GatewayBot("BOT_TOKEN")

# As a note, unless event_managed=False is passed here then this client
# will be managed based on gateway startup and stopping events.
# mention_prefix=True instructs the client to also set mention prefixes on the
# first startup.
client = tanjun.Client.from_gateway_bot(bot, declare_global_commands=True, mention_prefix=True)

component = tanjun.Component()
client.add_component(component)

# Declare a message command with some basic parser logic.
@component.with_command
@tanjun.with_greedy_argument("name", default="World")
@tanjun.as_message_command("test")
async def test_command(ctx: tanjun.abc.Context, name: str) -> None:
    await ctx.respond(f"Hello, {name}!")

# Declare a ping slash command
@component.with_command
@tanjun.with_user_slash_option("user", "The user facing command option's description", default=None)
@tanjun.as_slash_command("hello", "The command's user facing description")
async def hello(ctx: tanjun.abc.Context, user: typing.Optional[hikari.User]) -> None:
    user = user or ctx.author
    await ctx.respond(f"Hello, {user}!")
```

Alternatively, the client can also be built from a RESTBot but this will only
enable slash (interaction) command execution:

```py
bot = hikari.RESTBot("BOT_TOKEN", "Bot")

# declare_global_commands=True instructs the client to set the global commands
# for the relevant bot on first startup (this will replace any previously
# declared commands).
client = tanjun.Client.from_rest_bot(bot, declare_global_commands=True)

# This will load components from modules based on loader functions.
# For more information on this see `tanjun.as_loader`.
client.load_modules("module.paths")

# Note, unlike a gateway bound bot, the rest bot will not automatically start
# itself due to the lack of Hikari lifetime events in this environment and
# will have to be started after the Hikari client.
async def main() -> None:
    await bot.start()
    async with client.open():
        await bot.join()
```

For more extensive examples see the
[repository's examples](https://github.com/FasterSpeeding/Tanjun/tree/master/examples).
"""
from __future__ import annotations

__all__: list[str] = [
    # __init__.py
    "__author__",
    "__ci__",
    "__copyright__",
    "__coverage__",
    "__docs__",
    "__email__",
    "__issue_tracker__",
    "__license__",
    "__url__",
    "__version__",
    # abc.py
    "abc",
    "ClientCallbackNames",
    # checks.py
    "checks",
    "with_all_checks",
    "with_any_checks",
    "with_check",
    "with_dm_check",
    "with_guild_check",
    "with_nsfw_check",
    "with_sfw_check",
    "with_owner_check",
    "with_author_permission_check",
    "with_own_permission_check",
    # clients.py
    "clients",
    "as_loader",
    "as_unloader",
    "Client",
    "MessageAcceptsEnum",
    # commands.py
    "commands",
    "as_message_command",
    "as_message_command_group",
    "as_slash_command",
    "slash_command_group",
    "MessageCommand",
    "MessageCommandGroup",
    "SlashCommand",
    "SlashCommandGroup",
    "with_str_slash_option",
    "with_int_slash_option",
    "with_float_slash_option",
    "with_bool_slash_option",
    "with_role_slash_option",
    "with_user_slash_option",
    "with_member_slash_option",
    "with_channel_slash_option",
    "with_mentionable_slash_option",
    # components.py
    "components",
    "Component",
    # context.py
    "context",
    # conversion.py
    "conversion",
    "to_bool",
    "to_channel",
    "to_color",
    "to_colour",
    "to_datetime",
    "to_emoji",
    "to_guild",
    "to_invite",
    "to_invite_with_metadata",
    "to_member",
    "to_presence",
    "to_role",
    "to_snowflake",
    "to_user",
    "to_voice_state",
    # dependencies.py
    "dependencies",
    "BucketResource",
    "cached_inject",
    "inject_lc",
    "InMemoryConcurrencyLimiter",
    "InMemoryCooldownManager",
    "LazyConstant",
    "with_concurrency_limit",
    "with_cooldown",
    # errors.py
    "errors",
    "CommandError",
    "ConversionError",
    "FailedCheck",
    "FailedModuleLoad",
    "FailedModuleUnload",
    "HaltExecution",
    "MissingDependencyError",
    "ModuleMissingLoaders",
    "ModuleStateConflict",
    "NotEnoughArgumentsError",
    "TooManyArgumentsError",
    "ParserError",
    "TanjunError",
    # hooks.py
    "hooks",
    "AnyHooks",
    "Hooks",
    "MessageHooks",
    "SlashHooks",
    # injecting.py
    "injecting",
    "as_self_injecting",
    "inject",
    "injected",
    # parsing.py
    "parsing",
    "ShlexParser",
    "with_argument",
    "with_greedy_argument",
    "with_multi_argument",
    "with_option",
    "with_multi_option",
    "with_parser",
    # utilities.py
    "utilities",
    # repeaters.py
    "schedules",
    "as_interval",
]

import typing

from . import abc
from . import context
from . import utilities
from .abc import ClientCallbackNames
from .checks import with_all_checks
from .checks import with_any_checks
from .checks import with_author_permission_check
from .checks import with_check
from .checks import with_dm_check
from .checks import with_guild_check
from .checks import with_nsfw_check
from .checks import with_own_permission_check
from .checks import with_owner_check
from .checks import with_sfw_check
from .clients import Client
from .clients import MessageAcceptsEnum
from .clients import as_loader
from .clients import as_unloader
from .commands import MessageCommand
from .commands import MessageCommandGroup
from .commands import SlashCommand
from .commands import SlashCommandGroup
from .commands import as_message_command
from .commands import as_message_command_group
from .commands import as_slash_command
from .commands import slash_command_group
from .commands import with_bool_slash_option
from .commands import with_channel_slash_option
from .commands import with_float_slash_option
from .commands import with_int_slash_option
from .commands import with_member_slash_option
from .commands import with_mentionable_slash_option
from .commands import with_role_slash_option
from .commands import with_str_slash_option
from .commands import with_user_slash_option
from .components import Component
from .conversion import to_bool
from .conversion import to_channel
from .conversion import to_color
from .conversion import to_colour
from .conversion import to_datetime
from .conversion import to_emoji
from .conversion import to_guild
from .conversion import to_invite
from .conversion import to_invite_with_metadata
from .conversion import to_member
from .conversion import to_presence
from .conversion import to_role
from .conversion import to_snowflake
from .conversion import to_user
from .conversion import to_voice_state
from .dependencies import BucketResource
from .dependencies import InMemoryConcurrencyLimiter
from .dependencies import InMemoryCooldownManager
from .dependencies import LazyConstant
from .dependencies import cached_inject
from .dependencies import inject_lc
from .dependencies import with_concurrency_limit
from .dependencies import with_cooldown
from .errors import CommandError
from .errors import ConversionError
from .errors import FailedCheck
from .errors import FailedModuleLoad
from .errors import FailedModuleUnload
from .errors import HaltExecution
from .errors import MissingDependencyError
from .errors import ModuleMissingLoaders
from .errors import ModuleStateConflict
from .errors import NotEnoughArgumentsError
from .errors import ParserError
from .errors import TanjunError
from .errors import TooManyArgumentsError
from .hooks import AnyHooks
from .hooks import Hooks
from .hooks import MessageHooks
from .hooks import SlashHooks
from .injecting import as_self_injecting
from .injecting import inject
from .injecting import injected
from .parsing import ShlexParser
from .parsing import with_argument
from .parsing import with_greedy_argument
from .parsing import with_multi_argument
from .parsing import with_multi_option
from .parsing import with_option
from .parsing import with_parser
from .schedules import as_interval

__author__: typing.Final[str] = "Faster Speeding"
__ci__: typing.Final[str] = "https://github.com/FasterSpeeding/Tanjun/actions"
__copyright__: typing.Final[str] = "© 2020-2021 Faster Speeding"
__coverage__: typing.Final[str] = "https://codeclimate.com/github/FasterSpeeding/Tanjun"
__docs__: typing.Final[str] = "https://tanjun.cursed.solutions/"
__email__: typing.Final[str] = "lucina@lmbyrne.dev"
__issue_tracker__: typing.Final[str] = "https://github.com/FasterSpeeding/Tanjun/issues"
__license__: typing.Final[str] = "BSD"
__url__: typing.Final[str] = "https://github.com/FasterSpeeding/Tanjun"
__version__: typing.Final[str] = "2.3.2a1"
#   __author__: Final[str] = 'Faster Speeding'
#   __ci__: Final[str] = 'https://github.com/FasterSpeeding/Tanjun/actions'
#   __coverage__: Final[str] = 'https://codeclimate.com/github/FasterSpeeding/Tanjun'
#   __docs__: Final[str] = 'https://tanjun.cursed.solutions/'
#   __email__: Final[str] = 'lucina@lmbyrne.dev'
#   __issue_tracker__: Final[str] = 'https://github.com/FasterSpeeding/Tanjun/issues'
#   __license__: Final[str] = 'BSD'
#   __url__: Final[str] = 'https://github.com/FasterSpeeding/Tanjun'
#   __version__: Final[str] = '2.3.2a1'
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Interfaces of the objects and clients used within Tanjun."""
from __future__ import annotations

__all__: list[str] = [
    "ClientLoader",
    "BaseSlashCommandT",
    "CommandCallbackSig",
    "CommandCallbackSigT",
    "CheckSig",
    "CheckSigT",
    "Context",
    "ClientCallbackNames",
    "Hooks",
    "MetaEventSig",
    "MetaEventSigT",
    "AnyHooks",
    "MessageHooks",
    "SlashHooks",
    "ExecutableCommand",
    "HookSig",
    "HookSigT",
    "ErrorHookSig",
    "ErrorHookSigT",
    "ListenerCallbackSig",
    "ListenerCallbackSigT",
    "MaybeAwaitableT",
    "MessageCommand",
    "MessageCommandT",
    "MessageCommandGroup",
    "MessageContext",
    "BaseSlashCommand",
    "SlashCommand",
    "SlashCommandGroup",
    "SlashContext",
    "SlashOption",
    "Component",
    "Client",
]

import abc
import enum
import typing
from collections import abc as collections

import hikari

if typing.TYPE_CHECKING:
    import asyncio
    import datetime
    import pathlib

    from hikari import traits as hikari_traits


_T = typing.TypeVar("_T")


MaybeAwaitableT = typing.Union[_T, collections.Awaitable[_T]]
"""Type hint for a value which may need to be awaited to be resolved."""

ContextT = typing.TypeVar("ContextT", bound="Context")
ContextT_co = typing.TypeVar("ContextT_co", covariant=True, bound="Context")
ContextT_contra = typing.TypeVar("ContextT_contra", bound="Context", contravariant=True)
MetaEventSig = collections.Callable[..., MaybeAwaitableT[None]]
MetaEventSigT = typing.TypeVar("MetaEventSigT", bound="MetaEventSig")
BaseSlashCommandT = typing.TypeVar("BaseSlashCommandT", bound="BaseSlashCommand")
MessageCommandT = typing.TypeVar("MessageCommandT", bound="MessageCommand[typing.Any]")


CommandCallbackSig = collections.Callable[..., collections.Awaitable[None]]
"""Type hint of the callback a `Command` instance will operate on.

This will be called when executing a command and will need to take at least one
positional argument of type `Context` where any other required or optional
keyword or positional arguments will be based on the parser instance for the
command if applicable.

.. note::
    This will have to be asynchronous.
"""

CommandCallbackSigT = typing.TypeVar("CommandCallbackSigT", bound=CommandCallbackSig)
"""Generic equivalent of `CommandCallbackSig`."""

CheckSig = collections.Callable[..., MaybeAwaitableT[bool]]
"""Type hint of a general context check used with Tanjun `ExecutableCommand` classes.

This may be registered with a `ExecutableCommand` to add a rule which decides whether
it should execute for each context passed to it. This should take one positional
argument of type `Context` and may either be a synchronous or asynchronous
callback which returns `bool` where returning `False` or
raising `tanjun.errors.FailedCheck` will indicate that the current context
shouldn't lead to an execution.
"""

CheckSigT = typing.TypeVar("CheckSigT", bound=CheckSig)
"""Generic equivalent of `CheckSig`"""

HookSig = collections.Callable[..., MaybeAwaitableT[None]]
"""Type hint of the callback used as a general command hook.

.. note::
    This may be asynchronous or synchronous, dependency injection is supported
    for this callback's keyword arguments and the positional arguments which
    are passed dependent on the type of hook this is being registered as.
"""

HookSigT = typing.TypeVar("HookSigT", bound=HookSig)
"""Generic equivalent of `HookSig`."""

ErrorHookSig = collections.Callable[..., MaybeAwaitableT[typing.Optional[bool]]]
"""Type hint of the callback used as a unexpected command error hook.

This will be called whenever an unexpected `Exception` is raised during the
execution stage of a command (not including expected `tanjun.errors.TanjunError`).

This should take two positional arguments - of type `tanjun.abc.Context` and
`Exception` - and may be either a synchronous or asynchronous callback which
returns `bool` or `None` and may take advantage of dependency injection.

`True` is returned to indicate that the exception should be suppressed and
`False` is returned to indicate that the exception should be re-raised.
"""

ErrorHookSigT = typing.TypeVar("ErrorHookSigT", bound=ErrorHookSig)
"""Generic equivalent of `ErrorHookSig`."""

ListenerCallbackSig = collections.Callable[..., collections.Coroutine[typing.Any, typing.Any, None]]
"""Type hint of a hikari event manager callback.

This is guaranteed one positional arg of type `hikari.Event` regardless
of implementation and must be a coruotine function which returns `None`.
"""

ListenerCallbackSigT = typing.TypeVar("ListenerCallbackSigT", bound=ListenerCallbackSig)
"""Generic equivalent of `ListenerCallbackSig`."""


class Context(abc.ABC):
    """Interface for the context of a command execution."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def author(self) -> hikari.User:
        """Object of the user who triggered this command."""

    @property
    @abc.abstractmethod
    def channel_id(self) -> hikari.Snowflake:
        """ID of the channel this command was triggered in."""

    @property
    @abc.abstractmethod
    def cache(self) -> typing.Optional[hikari.api.Cache]:
        """Hikari cache instance this context's command client was initialised with."""

    @property
    @abc.abstractmethod
    def client(self) -> Client:
        """Tanjun `Client` implementation this context was spawned by."""

    @property
    @abc.abstractmethod
    def component(self) -> typing.Optional[Component]:
        """Object of the `Component` this context is bound to.

        .. note::
            This will only be `None` before this has been bound to a
            specific command but never during command execution nor checks.
        """

    @property  # TODO: can we somehow have this always be present on the command execution facing interface
    @abc.abstractmethod
    def command(self: ContextT) -> typing.Optional[ExecutableCommand[ContextT]]:
        """Object of the command this context is bound to.

        .. note::
            This will only be `None` before this has been bound to a
            specific command but never during command execution.
        """

    @property
    @abc.abstractmethod
    def created_at(self) -> datetime.datetime:
        """When this context was created.

        .. note::
            This will either refer to a message or integration's creation date.
        """

    @property
    @abc.abstractmethod
    def events(self) -> typing.Optional[hikari.api.EventManager]:
        """Object of the event manager this context's client was initialised with."""

    @property
    @abc.abstractmethod
    def guild_id(self) -> typing.Optional[hikari.Snowflake]:
        """ID of the guild this command was executed in.

        Will be `None` for all DM command executions.
        """

    @property
    @abc.abstractmethod
    def has_responded(self) -> bool:
        """Whether an initial response has been made for this context."""

    @property
    @abc.abstractmethod
    def is_human(self) -> bool:
        """Whether this command execution was triggered by a human.

        Will be `False` for bot and webhook triggered commands.
        """

    @property
    @abc.abstractmethod
    def member(self) -> typing.Optional[hikari.Member]:
        """Guild member object of this command's author.

        Will be `None` for DM command executions.
        """

    @property
    @abc.abstractmethod
    def server(self) -> typing.Optional[hikari.api.InteractionServer]:
        """Object of the Hikari interaction server provided for this context's client."""

    @property
    @abc.abstractmethod
    def rest(self) -> hikari.api.RESTClient:
        """Object of the Hikari REST client this context's client was initialised with."""

    @property
    @abc.abstractmethod
    def shards(self) -> typing.Optional[hikari_traits.ShardAware]:
        """Object of the Hikari shard manager this context's client was initialised with."""

    @property
    def voice(self) -> typing.Optional[hikari.api.VoiceComponent]:
        """Object of the Hikari voice component this context's client was initialised with."""

    @property
    @abc.abstractmethod
    def triggering_name(self) -> str:
        """Command name this execution was triggered with."""

    @abc.abstractmethod
    def set_component(self: _T, _: typing.Optional[Component], /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    async def fetch_channel(self) -> hikari.TextableChannel:
        """Fetch the channel the context was invoked in.

        .. note::
            This performs an API call. Consider using `Context.get_channel`
            if you have `hikari.config.CacheComponents.GUILD_CHANNELS` cache component enabled.

        Returns
        -------
        hikari.TextableChannel
            The textable DM or guild channel the context was invoked in.

        Raises
        ------
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.ForbiddenError
            If you are missing the `READ_MESSAGES` permission in the channel.
        hikari.NotFoundError
            If the channel is not found.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """

    @abc.abstractmethod
    async def fetch_guild(self) -> typing.Optional[hikari.Guild]:
        """Fetch the guild the context was invoked in.

        .. note::
            This performs an API call. Consider using `Context.get_guild`
            if you have `hikari.config.CacheComponents.GUILDS` cache component enabled.

        Returns
        -------
        typing.Optional[hikari.Guild]
            An optional guild the context was invoked in.
            `None` will be returned if the guild was not found or the context was invoked in a DM channel .

        Raises
        ------
        hikari.ForbiddenError
            If you are not part of the guild.
        hikari.NotFoundError
            If the guild is not found.
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """

    @abc.abstractmethod
    def get_channel(self) -> typing.Optional[hikari.TextableGuildChannel]:
        """Retrieve the channel the context was invoked in from the cache.

        .. note::
            This method requires the `hikari.config.CacheComponents.GUILD_CHANNELS` cache component.

        Returns
        -------
        typing.Optional[hikari.TextableGuildChannel]
            An optional guild channel the context was invoked in.
            `None` will be returned if the channel was not found or if it
            is DM channel.
        """

    @abc.abstractmethod
    def get_guild(self) -> typing.Optional[hikari.Guild]:
        """Fetch the guild that the context was invoked in.

        .. note::
            This method requires `hikari.config.CacheComponents.GUILDS` cache component enabled.

        Returns
        -------
        typing.Optional[hikari.Guild]
            An optional guild the context was invoked in.
            `None` will be returned if the guild was not found.
        """

    @abc.abstractmethod
    async def delete_initial_response(self) -> None:
        """Delete the initial response after invoking this context.

        Raises
        ------
        LookupError, hikari.NotFoundError
            The last context has no initial response.
        """

    @abc.abstractmethod
    async def delete_last_response(self) -> None:
        """Delete the last response after invoking this context.

        Raises
        ------
        LookupError, hikari.NotFoundError
            The last context has no responses.
        """

    @abc.abstractmethod
    async def edit_initial_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        replace_attachments: bool = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        """Edit the initial response for this context.

        Parameters
        ----------
        content : hikari.UndefinedOr[typing.Any]
            The content to edit the initial response with.

            If provided, the message contents. If
            `hikari.UNDEFINED`, then nothing will be sent
            in the content. Any other value here will be cast to a
            `str`.

            If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg
            is provided, then this will instead update the embed. This allows
            for simpler syntax when sending an embed alone.

            Likewise, if this is a `hikari.Resource`, then the
            content is instead treated as an attachment if no `attachment` and
            no `attachments` kwargs are provided.

        Other Parameters
        ----------------
        delete_after : typing.Union[datetime.timedelta, float, int, None]
            If provided, the seconds after which the response message should be deleted.

            .. note::
                Slash command responses can only be deleted within 14 minutes of the
                command being received.

            .. note::
                Since (as of writing) ephemeral responses cannot be deleted by the bot,
                this is ignored for ephemeral slash command responses.
        attachment : hikari.UndefinedOr[hikari.Resourceish]
            A singular attachment to edit the initial response with.
        attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]]
            A sequence of attachments to edit the initial response with.
        component : hikari.UndefinedNoneOr[hikari.api.ComponentBuilder]
            If provided, builder object of the component to set for this message.
            This component will replace any previously set components and passing
            `None` will remove all components.
        components : hikari.UndefinedNoneOr[collections.abc.Sequence[hikari.api.ComponentBuilder]]
            If provided, a sequence of the component builder objects set for
            this message. These components will replace any previously set
            components and passing `None` or an empty sequence will
            remove all components.
        embed : hikari.UndefinedOr[hikari.Embed]
            An embed to replace the initial response with.
        embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]]
            A sequence of embeds to replace the initial response with.
        replace_attachments : bool
            Whether to replace the attachments of the response or not. Default to `False`.
        mentions_everyone : hikari.UndefinedOr[bool]
            If provided, whether the message should parse @everyone/@here
            mentions.
        user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.
            Alternatively this may be a collection of
            `hikari.Snowflake`, or `hikari.PartialUser`
            derivatives to enforce mentioning specific users.
        role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.
            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialRole` derivatives to enforce mentioning
            specific roles.

        Notes
        -----
        Attachments can be passed as many different things, to aid in
        convenience.
        * If a `pathlib.PurePath` or `str` to a valid URL, the
            resource at the given URL will be streamed to Discord when
            sending the message. Subclasses of
            `hikari.WebResource` such as
            `hikari.URL`,
            `hikari.Attachment`,
            `hikari.Emoji`,
            `EmbedResource`, etc will also be uploaded this way.
            This will use bit-inception, so only a small percentage of the
            resource will remain in memory at any one time, thus aiding in
            scalability.
        * If a `hikari.Bytes` is passed, or a `str`
            that contains a valid data URI is passed, then this is uploaded
            with a randomized file name if not provided.
        * If a `hikari.File`, `pathlib.PurePath` or
            `str` that is an absolute or relative path to a file
            on your file system is passed, then this resource is uploaded
            as an attachment using non-blocking code internally and streamed
            using bit-inception where possible. This depends on the
            type of `concurrent.futures.Executor` that is being used for
            the application (default is a thread pool which supports this
            behaviour).

        Returns
        -------
        hikari.Message
            The message that has been edited.

        Raises
        ------
        ValueError
            If more than 100 unique objects/entities are passed for
            `role_mentions` or `user_mentions`.
            If `delete_after` would be more than 14 minutes after the slash
            command was called.
        TypeError
            If both `attachment` and `attachments` are specified.
        hikari.BadRequestError
            This may be raised in several discrete situations, such as messages
            being empty with no attachments or embeds; messages with more than
            2000 characters in them, embeds that exceed one of the many embed
            limits; too many attachments; attachments that are too large;
            invalid image URLs in embeds; too many components.
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.ForbiddenError
            If you are missing the `SEND_MESSAGES` in the channel or the
            person you are trying to message has the DM's disabled.
        hikari.NotFoundError
            If the channel is not found.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """

    @abc.abstractmethod
    async def edit_last_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        replace_attachments: bool = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        """Edit the last response for this context.

        Parameters
        ----------
        content : hikari.UndefinedOr[typing.Any]
            The content to edit the last response with.

            If provided, the message contents. If
            `hikari.UNDEFINED`, then nothing will be sent
            in the content. Any other value here will be cast to a
            `str`.

            If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg
            is provided, then this will instead update the embed. This allows
            for simpler syntax when sending an embed alone.

            Likewise, if this is a `hikari.Resource`, then the
            content is instead treated as an attachment if no `attachment` and
            no `attachments` kwargs are provided.

        Other Parameters
        ----------------
        delete_after : typing.Union[datetime.timedelta, float, int, None]
            If provided, the seconds after which the response message should be deleted.

            .. note::
                Slash command responses can only be deleted within 14 minutes of the
                command being received.

            .. note::
                Since (as of writing) ephemeral responses cannot be deleted by the bot,
                this is ignored for ephemeral slash command responses.
        attachment : hikari.UndefinedOr[hikari.Resourceish]
            A singular attachment to edit the last response with.
        attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]]
            A sequence of attachments to edit the last response with.
        component : hikari.UndefinedNoneOr[hikari.api.ComponentBuilder]
            If provided, builder object of the component to set for this message.
            This component will replace any previously set components and passing
            `None` will remove all components.
        components : hikari.UndefinedNoneOr[collections.abc.Sequence[hikari.api.ComponentBuilder]]
            If provided, a sequence of the component builder objects set for
            this message. These components will replace any previously set
            components and passing `None` or an empty sequence will
            remove all components.
        embed : hikari.UndefinedOr[hikari.Embed]
            An embed to replace the last response with.
        embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]]
            A sequence of embeds to replace the last response with.
        replace_attachments : bool
            Whether to replace the attachments of the response or not. Default to `False`.
        mentions_everyone : hikari.UndefinedOr[bool]
            If provided, whether the message should parse @everyone/@here
            mentions.
        user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or `hikari.PartialUser`
            derivatives to enforce mentioning specific users.
        role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialRole` derivatives to enforce mentioning
            specific roles.

        Notes
        -----
        Attachments can be passed as many different things, to aid in
        convenience.
        * If a `pathlib.PurePath` or `str` to a valid URL, the
            resource at the given URL will be streamed to Discord when
            sending the message. Subclasses of
            `hikari.WebResource` such as
            `hikari.URL`,
            `hikari.Attachment`,
            `hikari.Emoji`,
            `EmbedResource`, etc will also be uploaded this way.
            This will use bit-inception, so only a small percentage of the
            resource will remain in memory at any one time, thus aiding in
            scalability.
        * If a `hikari.Bytes` is passed, or a `str`
            that contains a valid data URI is passed, then this is uploaded
            with a randomized file name if not provided.
        * If a `hikari.File`, `pathlib.PurePath` or
            `str` that is an absolute or relative path to a file
            on your file system is passed, then this resource is uploaded
            as an attachment using non-blocking code internally and streamed
            using bit-inception where possible. This depends on the
            type of `concurrent.futures.Executor` that is being used for
            the application (default is a thread pool which supports this
            behaviour).

        Returns
        -------
        hikari.Message
            The message that has been edited.

        Raises
        ------
        ValueError
            If more than 100 unique objects/entities are passed for
            `role_mentions` or `user_mentions`.
            If `delete_after` would be more than 14 minutes after the slash
            command was called.
        TypeError
            If both `attachment` and `attachments` are specified.
        hikari.BadRequestError
            This may be raised in several discrete situations, such as messages
            being empty with no attachments or embeds; messages with more than
            2000 characters in them, embeds that exceed one of the many embed
            limits; too many attachments; attachments that are too large;
            invalid image URLs in embeds; too many components.
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.ForbiddenError
            If you are missing the `SEND_MESSAGES` in the channel or the
            person you are trying to message has the DM's disabled.
        hikari.NotFoundError
            If the channel is not found.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """

    @abc.abstractmethod
    async def fetch_initial_response(self) -> hikari.Message:
        """Fetch the initial response for this context.

        Raises
        ------
        LookupError, hikari.NotFoundError
            The response was not found.
        """

    @abc.abstractmethod
    async def fetch_last_response(self) -> hikari.Message:
        """Fetch the last response for this context.

        Raises
        ------
        LookupError, hikari.NotFoundError
            The response was not found.
        """

    @typing.overload
    @abc.abstractmethod
    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: typing.Literal[False] = False,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> typing.Optional[hikari.Message]:
        ...

    @typing.overload
    @abc.abstractmethod
    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: typing.Literal[True],
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        ...

    @abc.abstractmethod
    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: bool = False,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> typing.Optional[hikari.Message]:
        """Respond to this context.

        Parameters
        ----------
        content : hikari.UndefinedOr[typing.Any]
            The content to respond with.

            If provided, the message contents. If
            `hikari.UNDEFINED`, then nothing will be sent
            in the content. Any other value here will be cast to a
            `str`.

            If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg
            is provided, then this will instead update the embed. This allows
            for simpler syntax when sending an embed alone.

            Likewise, if this is a `hikari.Resource`, then the
            content is instead treated as an attachment if no `attachment` and
            no `attachments` kwargs are provided.

        Other Parameters
        ----------------
        ensure_result : bool
            Ensure that this call will always return a message object.

            If `True` then this will always return `hikari.Message`, otherwise
            this will return `Optional[hikari.Message]`.

            It's worth noting that, under certain scenarios within the slash
            command flow, this may lead to an extre request being made.
        delete_after : typing.Union[datetime.timedelta, float, int, None]
            If provided, the seconds after which the response message should be deleted.

            .. note::
                Slash command responses can only be deleted within 14 minutes of the
                command being received.

            .. note::
                Since (as of writing) ephemeral responses cannot be deleted by the bot,
                this is ignored for ephemeral slash command responses.
        component : hikari.UndefinedOr[hikari.api.ComponentBuilder]
            If provided, builder object of the component to include in this response.
        components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]]
            If provided, a sequence of the component builder objects to include
            in this response.
        embed : hikari.UndefinedOr[hikari.Embed]
            An embed to respond with.
        embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]]
            A sequence of embeds to respond with.
        mentions_everyone : hikari.UndefinedOr[bool]
            If provided, whether the message should parse @everyone/@here
            mentions.
        user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or `hikari.PartialUser`
            derivatives to enforce mentioning specific users.
        role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialRole` derivatives to enforce mentioning
            specific roles.

        Returns
        -------
        typing.Optional[hikari.Message]
            The message that has been created if it was immedieatly available or
            `ensure_result` was set to `True`, else `None`.

        Raises
        ------
        ValueError
            If more than 100 unique objects/entities are passed for
            `role_mentions` or `user_mentions`.
            If `delete_after` would be more than 14 minutes after the slash
            command was called.
        TypeError
            If both `attachment` and `attachments` are specified.
        hikari.BadRequestError
            This may be raised in several discrete situations, such as messages
            being empty with no attachments or embeds; messages with more than
            2000 characters in them, embeds that exceed one of the many embed
            limits; too many attachments; attachments that are too large;
            invalid image URLs in embeds; too many components.
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.ForbiddenError
            If you are missing the `SEND_MESSAGES` in the channel or the
            person you are trying to message has the DM's disabled.
        hikari.NotFoundError
            If the channel is not found.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """


class MessageContext(Context, abc.ABC):
    __slots__ = ()

    @property
    @abc.abstractmethod
    def command(self) -> typing.Optional[MessageCommand[typing.Any]]:
        """Command that was invoked.

        .. note::
            This is always set during command, command check and parser
            converter execution but isn't guaranteed during client callback
            nor client/component check execution.
        """

    @property
    @abc.abstractmethod
    def content(self) -> str:
        """Content of the context's message minus the triggering name and prefix."""

    @property
    @abc.abstractmethod
    def message(self) -> hikari.Message:
        """Message that triggered the context."""

    @property
    @abc.abstractmethod
    def shard(self) -> typing.Optional[hikari.api.GatewayShard]:
        """Shard that triggered the context.

        .. note::
            This will be `None` if `ctx.shards` is also `None`.
        """

    @property
    @abc.abstractmethod
    def triggering_prefix(self) -> str:
        """Prefix that triggered the context."""

    @property
    @abc.abstractmethod
    def triggering_name(self) -> str:
        """Command name that triggered the context."""

    @abc.abstractmethod
    def set_command(self: _T, _: typing.Optional[MessageCommand[typing.Any]], /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def set_content(self: _T, _: str, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def set_triggering_name(self: _T, _: str, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: bool = True,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        nonce: hikari.UndefinedOr[str] = hikari.UNDEFINED,
        reply: typing.Union[bool, hikari.SnowflakeishOr[hikari.PartialMessage], hikari.UndefinedType] = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        mentions_reply: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        """Respond to this context.

        Parameters
        ----------
        content : hikari.UndefinedOr[typing.Any]
            The content to respond with.

            If provided, the message contents. If
            `hikari.UNDEFINED`, then nothing will be sent
            in the content. Any other value here will be cast to a
            `str`.

            If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg
            is provided, then this will instead update the embed. This allows
            for simpler syntax when sending an embed alone.

            Likewise, if this is a `hikari.Resource`, then the
            content is instead treated as an attachment if no `attachment` and
            no `attachments` kwargs are provided.

        Other Parameters
        ----------------
        ensure_result : bool
            Ensure this method call will return a message object.

            This does nothing for message command contexts as the result w ill
            always be immedieatly available.
        delete_after : typing.Union[datetime.timedelta, float, int, None]
            If provided, the seconds after which the response message should be deleted.
        tts : hikari.UndefinedOr[bool]
            Whether to respond with tts/text to speech or no.
        reply : typing.Union[bool, hikari.SnowflakeishOr[hikari.PartialMessage], hikari.UndefinedType]
            Whether to reply instead of sending the content to the context.

            Defaults to `hikari.UNDEFINED`.
            Passing `True` here indicates a reply to `MessageContext.message`.
        nonce : hikari.UndefinedOr[str]
            The nonce that validates that the message was sent.
        attachment : hikari.UndefinedOr[hikari.Resourceish]
            A singular attachment to respond with.
        attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]]
            A sequence of attachments to respond with.
        component : hikari.UndefinedOr[hikari.api.ComponentBuilder]
            If provided, builder object of the component to include in this message.
        components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]]
            If provided, a sequence of the component builder objects to include
            in this message.
        embed : hikari.UndefinedOr[hikari.Embed]
            An embed to respond with.
        embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]]
            A sequence of embeds to respond with.
        mentions_everyone : hikari.UndefinedOr[bool]
            If provided, whether the message should parse @everyone/@here
            mentions.
        user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or `hikari.PartialUser`
            derivatives to enforce mentioning specific users.
        role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.
            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialRole` derivatives to enforce mentioning
            specific roles.

        Notes
        -----
        Attachments can be passed as many different things, to aid in
        convenience.
        * If a `pathlib.PurePath` or `str` to a valid URL, the
            resource at the given URL will be streamed to Discord when
            sending the message. Subclasses of
            `hikari.WebResource` such as
            `hikari.URL`,
            `hikari.Attachment`,
            `hikari.Emoji`,
            `EmbedResource`, etc will also be uploaded this way.
            This will use bit-inception, so only a small percentage of the
            resource will remain in memory at any one time, thus aiding in
            scalability.
        * If a `hikari.Bytes` is passed, or a `str`
            that contains a valid data URI is passed, then this is uploaded
            with a randomized file name if not provided.
        * If a `hikari.File`, `pathlib.PurePath` or
            `str` that is an absolute or relative path to a file
            on your file system is passed, then this resource is uploaded
            as an attachment using non-blocking code internally and streamed
            using bit-inception where possible. This depends on the
            type of `concurrent.futures.Executor` that is being used for
            the application (default is a thread pool which supports this
            behaviour).

        Returns
        -------
        hikari.Message
            The message that has been created.

        Raises
        ------
        ValueError
            If more than 100 unique objects/entities are passed for
            `role_mentions` or `user_mentions`.

            If the interaction will have expired before `delete_after` is reached.
        TypeError
            If both `attachment` and `attachments` are specified.
        hikari.BadRequestError
            This may be raised in several discrete situations, such as messages
            being empty with no attachments or embeds; messages with more than
            2000 characters in them, embeds that exceed one of the many embed
            limits; too many attachments; attachments that are too large;
            invalid image URLs in embeds; if `reply` is not found or not in the
            same channel as `channel`; too many components.
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.ForbiddenError
            If you are missing the `SEND_MESSAGES` in the channel or the
            person you are trying to message has the DM's disabled.
        hikari.NotFoundError
            If the channel is not found.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """


class SlashOption(abc.ABC):
    """Interface of slash command option with extra logic to help resolve it."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def name(self) -> str:
        """Name of this option."""

    @property
    @abc.abstractmethod
    def type(self) -> typing.Union[hikari.OptionType, int]:
        """Type of this option."""

    @property
    @abc.abstractmethod
    def value(self) -> typing.Union[str, hikari.Snowflake, int, bool, float]:
        """Value provided for this option.

        .. note::
            For discord entity option types (user, member, channel and role)
            this will be the entity's ID.
        """

    @abc.abstractmethod
    def boolean(self) -> bool:
        """Get the boolean value of this option.

        Raises
        ------
        TypeError
            If `SlashOption.type` is not BOOLEAN.
        """

    @abc.abstractmethod
    def float(self) -> float:
        """Get the float value of this option.

        Raises
        ------
        TypeError
            If `SlashOption.type` is not FLOAT.
        ValueError
            If called on the focused option for an autocomplete interaction
            when it's a malformed (incomplete) float.
        """

    @abc.abstractmethod
    def integer(self) -> int:
        """Get the integer value of this option.

        Raises
        ------
        TypeError
            If `SlashOption.type` is not INTEGER.
        ValueError
            If called on the focused option for an autocomplete interaction
            when it's a malformed (incomplete) integer.
        """

    @abc.abstractmethod
    def snowflake(self) -> hikari.Snowflake:
        """Get the ID of this option.

        Raises
        ------
        TypeError
            If `SlashOption.type` is not one of CHANNEL, MENTIONABLE, ROLE
            or USER.
        """

    @abc.abstractmethod
    def string(self) -> str:
        """Get the string value of this option.

        Raises
        ------
        TypeError
            If `SlashOption.type` is not STRING.
        """

    @abc.abstractmethod
    def resolve_value(
        self,
    ) -> typing.Union[hikari.InteractionChannel, hikari.InteractionMember, hikari.Role, hikari.User]:
        """Resolve this option to an object value.

        Returns
        -------
        typing.Union[hikari.InteractionChannel, hikari.InteractionMember, hikari.Role, hikari.User]
            The object value of this option.

        Raises
        ------
        TypeError
            If the option isn't resolvable.
        """

    @abc.abstractmethod
    def resolve_to_channel(self) -> hikari.InteractionChannel:
        """Resolve this option to a channel object.

        Returns
        -------
        hikari.InteractionChannel
            The channel object.

        Raises
        ------
        TypeError
            If the option is not a channel and a `default` wasn't provided.
        """

    @abc.abstractmethod
    def resolve_to_member(self, *, default: _T = ...) -> typing.Union[hikari.InteractionMember, _T]:
        """Resolve this option to a member object.

        Other Parameters
        ----------------
        default:
            The default value to return if this option cannot be resolved.

            If this is not provided, this method will raise a `TypeError` if
            this option cannot be resolved.

        Returns
        -------
        typing.Union[hikari.InteractionMember, _T]
            The member object or `default` if it was provided and this option
            was a user type but had no member.

        Raises
        ------
        LookupError
            If no member was found for this option and a `default` wasn't provided.

            This includes if the option is a mentionable type which targets a
            member-less user.

            This could happen if the user isn't in the current guild or if this
            command was executed in a DM and this option should still be resolvable
            to a user.
        TypeError
            If the option is not a user option and a `default` wasn't provided.

            This includes if the option is a mentionable type but doesn't
            target a user.
        """

    @abc.abstractmethod
    def resolve_to_mentionable(self) -> typing.Union[hikari.Role, hikari.User, hikari.Member]:
        """Resolve this option to a mentionable object.

        Returns
        -------
        typing.Union[hikari.Role, hikari.User, hikari.Member]
            The mentionable object.

        Raises
        ------
        TypeError
            If the option is not a mentionable, user or role type.
        """

    @abc.abstractmethod
    def resolve_to_role(self) -> hikari.Role:
        """Resolve this option to a role object.

        Returns
        -------
        hikari.Role
            The role object.

        Raises
        ------
        TypeError
            If the option is not a role.

            This includes mentionable options which point towards a user.
        """

    @abc.abstractmethod
    def resolve_to_user(self) -> typing.Union[hikari.User, hikari.Member]:
        """Resolve this option to a user object.

        .. note::
            This will resolve to a `hikari.Member` first if the relevant
            command was executed within a guild and the option targeted one of
            the guild's members, otherwise it will resolve to `hikari.User`.

            It's also worth noting that hikari.Member inherits from hikari.User
            meaning that the return value of this can always be treated as a
            user.

        Returns
        -------
        typing.Union[hikari.User, hikari.Member]
            The user object.

        Raises
        ------
        TypeError
            If the option is not a user.

            This includes mentionable options which point towards a role.
        """


class SlashContext(Context, abc.ABC):
    """Interface of a slash command specific context."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def command(self) -> typing.Optional[BaseSlashCommand]:
        """Command that was invoked.

        .. note::
            This should always be set during command, command check execution
            and command hook execution but isn't guaranteed for client callbacks
            nor component/client checks.
        """

    @property
    @abc.abstractmethod
    def defaults_to_ephemeral(self) -> bool:
        """Whether the context is marked as defaulting to ephemeral response.

        This effects calls to `SlashContext.create_followup`,
        `SlashContext.create_initial_response`, `SlashContext.defer` and
        `SlashContext.respond` unless the `flags` field is provided for the
        methods which support it.
        """

    @property
    @abc.abstractmethod
    def expires_at(self) -> datetime.datetime:
        """When this application command context expires.

        After this time is reached, the message/response methods on this
        context will always raise `hikari.errors.NotFoundError`.
        """

    @property
    @abc.abstractmethod
    def has_been_deferred(self) -> bool:
        """Whether the initial response for this context has been deferred.

        .. warning::
            If this is `True` when `SlashContext.has_responded` is `False`
            then `SlashContext.edit_initial_response` will need to be used
            to create the initial response rather than
            `SlashContext.create_initial_response`.
        """

    @property
    @abc.abstractmethod
    def interaction(self) -> hikari.CommandInteraction:
        """Interaction this context is for."""

    @property
    @abc.abstractmethod
    def member(self) -> typing.Optional[hikari.InteractionMember]:
        """Object of the member that triggered this command if this is in a guild."""

    @property
    @abc.abstractmethod
    def options(self) -> collections.Mapping[str, SlashOption]:
        """Mapping of option names to the values provided for them."""

    @abc.abstractmethod
    def set_command(self: _T, _: typing.Optional[BaseSlashCommand], /) -> _T:
        """Set the command for this context.

        Parameters
        ----------
        command : typing.Optional[BaseSlashCommand]
            The command this context is for.
        """

    @abc.abstractmethod
    def set_ephemeral_default(self: _T, state: bool, /) -> _T:
        """Set the ephemeral default state for this context.

        Parameters
        ----------
        state : bool
            The new ephemeral default state.

            If this is `True` then all calls to the response creating methods
            on this context will default to being ephemeral.
        """

    @abc.abstractmethod
    async def defer(
        self, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED
    ) -> None:
        """Defer the initial response for this context.

        .. note::
            The ephemeral state of the first response is decided by whether the
            deferral is ephemeral.

        Other Parameters
        ----------------
        flags : typing.Union[hikari.UndefinedType, int, hikari.MessageFlag]
            The flags to use for the initial response.
        """

    @abc.abstractmethod
    async def mark_not_found(self) -> None:
        """Mark this context as not found.

        Dependent on how the client is configured this may lead to a not found
        response message being sent.
        """

    @abc.abstractmethod
    async def create_followup(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED,
    ) -> hikari.Message:
        """Create a followup response for this context.

        .. warning::
            Calling this on a context which hasn't had an initial response yet
            will lead to a `hikari.NotFoundError` being raised.

        Parameters
        ----------
        content : hikari.UndefinedOr[typing.Any]
            If provided, the message contents. If
            `hikari.UNDEFINED`, then nothing will be sent
            in the content. Any other value here will be cast to a
            `str`.

            If this is a `hikari.Embed` and no `embed` kwarg is
            provided, then this will instead update the embed. This allows for
            simpler syntax when sending an embed alone.

            Likewise, if this is a `hikari.Resource`, then the
            content is instead treated as an attachment if no `attachment` and
            no `attachments` kwargs are provided.

        Other Parameters
        ----------------
        delete_after : typing.Union[datetime.timedelta, float, int, None]
            If provided, the seconds after which the response message should be deleted.

            .. note::
                Slash command responses can only be deleted within 14 minutes of the
                command being received.

            .. note::
                Since (as of writing) ephemeral responses cannot be deleted by the bot,
                this is ignored for ephemeral slash command responses.
        attachment : hikari.UndefinedOr[hikari.Resourceish]
            If provided, the message attachment. This can be a resource,
            or string of a path on your computer or a URL.
        attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]]
            If provided, the message attachments. These can be resources, or
            strings consisting of paths on your computer or URLs.
        component : hikari.UndefinedOr[hikari.api.ComponentBuilder]
            If provided, builder object of the component to include in this message.
        components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]]
            If provided, a sequence of the component builder objects to include
            in this message.
        embed : hikari.UndefinedOr[hikari.Embed]
            If provided, the message embed.
        embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]]
            If provided, the message embeds.
        mentions_everyone : hikari.UndefinedOr[bool]
            If provided, whether the message should parse @everyone/@here
            mentions.
        user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialUser` derivatives to enforce mentioning
            specific users.
        role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]]
            If provided, and `True`, all mentions will be parsed.
            If provided, and `False`, no mentions will be parsed.
            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialRole` derivatives to enforce mentioning
            specific roles.
        tts : hikari.UndefinedOr[bool]
            If provided, whether the message will be sent as a TTS message.
        flags : typing.Union[hikari.UndefinedType, int, hikari.MessageFlag]
            The flags to set for this response.

            As of writing this can only flag which can be provided is EPHEMERAL,
            other flags are just ignored.

        Returns
        -------
        hikari.Message
            The created message object.

        Raises
        ------
        hikari.NotFoundError
            If the current interaction is not found or it hasn't had an initial
            response yet.
        hikari.BadRequestError
            This can be raised if the file is too large; if the embed exceeds
            the defined limits; if the message content is specified only and
            empty or greater than `2000` characters; if neither content, file
            or embeds are specified.
            If any invalid snowflake IDs are passed; a snowflake may be invalid
            due to it being outside of the range of a 64 bit integer.
        ValueError
            If more than 100 unique objects/entities are passed for
            `role_mentions` or `user_mentions.

            If the interaction will have expired before `delete_after` is reached.
        TypeError
            If both `attachment` and `attachments` are specified.
        """

    @abc.abstractmethod
    async def create_initial_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
        flags: typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
    ) -> None:
        """Create the initial response for this context.

        .. warning::
            Calling this on a context which already has an initial
            response will result in this raising a `hikari.NotFoundError`.
            This includes if the REST interaction server has already responded
            to the request and deferrals.

        Other Parameters
        ----------------
        delete_after : typing.Union[datetime.timedelta, float, int, None]
            If provided, the seconds after which the response message should be deleted.

            .. note::
                Slash command responses can only be deleted within 14 minutes of the
                command being received.

            .. note::
                Since (as of writing) ephemeral responses cannot be deleted by the bot,
                this is ignored for ephemeral slash command responses.
        content : hikari.UndefinedOr[typing.Any]
            If provided, the message contents. If
            `hikari.UNDEFINED`, then nothing will be sent
            in the content. Any other value here will be cast to a
            `str`.

            If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg
            is provided, then this will instead update the embed. This allows
            for simpler syntax when sending an embed alone.
        component : hikari.UndefinedOr[hikari.api.ComponentBuilder]
            If provided, builder object of the component to include in this message.
        components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]]
            If provided, a sequence of the component builder objects to include
            in this message.
        embed : hikari.UndefinedOr[hikari.Embed]
            If provided, the message embed.
        embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]]
            If provided, the message embeds.
        flags : typing.Union[int, hikari.MessageFlag, hikari.UndefinedType]
            If provided, the message flags this response should have.

            As of writing the only message flag which can be set here is
            `hikari.MessageFlag.EPHEMERAL`.
        tts : hikari.UndefinedOr[bool]
            If provided, whether the message will be read out by a screen
            reader using Discord's TTS (text-to-speech) system.
        mentions_everyone : hikari.UndefinedOr[bool]
            If provided, whether the message should parse @everyone/@here
            mentions.
        user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]]
            If provided, and `True`, all user mentions will be detected.
            If provided, and `False`, all user mentions will be ignored
            if appearing in the message body.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialUser` derivatives to enforce mentioning
            specific users.
        role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]]
            If provided, and `True`, all role mentions will be detected.
            If provided, and `False`, all role mentions will be ignored
            if appearing in the message body.

            Alternatively this may be a collection of
            `hikari.Snowflake`, or
            `hikari.PartialRole` derivatives to enforce mentioning
            specific roles.

        Raises
        ------
        ValueError
            If more than 100 unique objects/entities are passed for
            `role_mentions` or `user_mentions`.

            If the interaction will have expired before `delete_after` is reached.
        TypeError
            If both `embed` and `embeds` are specified.
        hikari.BadRequestError
            This may be raised in several discrete situations, such as messages
            being empty with no embeds; messages with more than
            2000 characters in them, embeds that exceed one of the many embed
            limits; invalid image URLs in embeds.
        hikari.UnauthorizedError
            If you are unauthorized to make the request (invalid/missing token).
        hikari.NotFoundError
            If the interaction is not found or if the interaction's initial
            response has already been created.
        hikari.RateLimitTooLongError
            Raised in the event that a rate limit occurs that is
            longer than `max_rate_limit` when making a request.
        hikari.RateLimitedError
            Usually, Hikari will handle and retry on hitting
            rate-limits automatically. This includes most bucket-specific
            rate-limits and global rate-limits. In some rare edge cases,
            however, Discord implements other undocumented rules for
            rate-limiting, such as limits per attribute. These cannot be
            detected or handled normally by Hikari due to their undocumented
            nature, and will trigger this exception if they occur.
        hikari.InternalServerError
            If an internal error occurs on Discord while handling the request.
        """


class Hooks(abc.ABC, typing.Generic[ContextT_contra]):
    """Interface of a collection of callbacks called during set stage of command execution."""

    __slots__ = ()

    @abc.abstractmethod
    def copy(self: _T) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def add_on_error(self: _T, callback: ErrorHookSig, /) -> _T:
        """Add an error callback to this hook object.

        .. note::
            This won't be called for expected `tanjun.TanjunError` derived errors.

        Parameters
        ----------
        callback : ErrorHookSig
            The callback to add to this hook.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `Exception`) and may be either
            synchronous or asynchronous.

            Returning `True` indicates that the error should be suppressed,
            `False` that it should be re-raised and `None` that no decision has
            been made. This will be accounted for along with the decisions
            other error hooks make by majority rule.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """

    @abc.abstractmethod
    def with_on_error(self, callback: ErrorHookSigT, /) -> ErrorHookSigT:
        """Add an error callback to this hook object through a decorator call.

        .. note::
            This won't be called for expected `tanjun.TanjunError` derived errors.

        Examples
        --------
        ```py
        hooks = AnyHooks()

        @hooks.with_on_error
        async def on_error(ctx: tanjun.abc.Context, error: Exception) -> bool:
            if isinstance(error, SomeExpectedType):
                await ctx.respond("You dun goofed")
                return True  # Indicating that it should be suppressed.

            await ctx.respond(f"An error occurred: {error}")
            return False  # Indicating that it should be re-raised
        ```

        Parameters
        ----------
        callback : ErrorHookSigT
            The callback to add to this hook.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `Exception`) and may be either
            synchronous or asynchronous.

            Returning `True` indicates that the error shoul be suppressed,
            `False` that it should be re-raised and `None` that no decision
            has been made. This will be accounted for along with the decisions
            other error hooks make by majority rule.

        Returns
        -------
        ErrorHookSigT
            The hook callback which was added.
        """

    @abc.abstractmethod
    def add_on_parser_error(self: _T, callback: HookSig, /) -> _T:
        """Add a parser error callback to this hook object.

        Parameters
        ----------
        callback : HookSig
            The callback to add to this hook.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`),
            return `None` and may be either synchronous or asynchronous.

            It's worth noting that this unlike general error handlers, this will
            always suppress the error.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """

    @abc.abstractmethod
    def with_on_parser_error(self, callback: HookSigT, /) -> HookSigT:
        """Add a parser error callback to this hook object through a decorator call.

        Examples
        --------
        ```py
        hooks = AnyHooks()

        @hooks.with_on_parser_error
        async def on_parser_error(ctx: tanjun.abc.Context, error: tanjun.errors.ParserError) -> None:
            await ctx.respond(f"You gave invalid input: {error}")
        ```

        Parameters
        ----------
        callback : HookSigT
            The parser error callback to add to this hook.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`),
            return `None` and may be either synchronous or asynchronous.

        Returns
        -------
        HookSigT
            The callback which was added.
        """

    @abc.abstractmethod
    def add_post_execution(self: _T, callback: HookSig, /) -> _T:
        """Add a post-execution callback to this hook object.

        Parameters
        ----------
        callback : HookSig
            The callback to add to this hook.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """

    @abc.abstractmethod
    def with_post_execution(self, callback: HookSigT, /) -> HookSigT:
        """Add a post-execution callback to this hook object through a decorator call.

        Examples
        --------
        ```py
        hooks = AnyHooks()

        @hooks.with_post_execution
        async def post_execution(ctx: tanjun.abc.Context) -> None:
            await ctx.respond("You did something")
        ```

        Parameters
        ----------
        callback : HookSigT
            The post-execution callback to add to this hook.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        HookSigT
            The post-execution callback which was seaddedt.
        """

    @abc.abstractmethod
    def add_pre_execution(self: _T, callback: HookSig, /) -> _T:
        """Add a pre-execution callback for this hook object.

        Parameters
        ----------
        callback : HookSig
            The callback to add to this hook.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """

    @abc.abstractmethod
    def with_pre_execution(self, callback: HookSigT, /) -> HookSigT:
        """Add a pre-execution callback to this hook object through a decorator call.

        Examples
        --------
        ```py
        hooks = AnyHooks()

        @hooks.with_pre_execution
        async def pre_execution(ctx: tanjun.abc.Context) -> None:
            await ctx.respond("You did something")
        ```

        Parameters
        ----------
        callback : HookSigT
            The pre-execution callback to add to this hook.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        HookSigT
            The pre-execution callback which was added.
        """

    @abc.abstractmethod
    def add_on_success(self: _T, callback: HookSig, /) -> _T:
        """Add a success callback to this hook object.

        Parameters
        ----------
        callback : HookSig
            The callback to add to this hook.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """

    @abc.abstractmethod
    def with_on_success(self, callback: HookSigT, /) -> HookSigT:
        """Add a success callback to this hook object through a decorator call.

        Examples
        --------
        ```py
        hooks = AnyHooks()

        @hooks.with_on_success
        async def on_success(ctx: tanjun.abc.Context) -> None:
            await ctx.respond("You did something")
        ```

        Parameters
        ----------
        callback : HookSigT
            The success callback to add to this hook.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        HookSigT
            The success callback which was added.
        """

    @abc.abstractmethod
    async def trigger_error(
        self,
        ctx: ContextT_contra,
        /,
        exception: Exception,
        *,
        hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None,
    ) -> int:
        raise NotImplementedError

    @abc.abstractmethod
    async def trigger_post_execution(
        self,
        ctx: ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None,
    ) -> None:
        raise NotImplementedError

    @abc.abstractmethod
    async def trigger_pre_execution(
        self,
        ctx: ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None,
    ) -> None:
        raise NotImplementedError

    @abc.abstractmethod
    async def trigger_success(
        self,
        ctx: ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None,
    ) -> None:
        raise NotImplementedError


AnyHooks = Hooks[Context]
"""Execution hooks for any context."""

MessageHooks = Hooks[MessageContext]
"""Execution hooks for messages commands."""

SlashHooks = Hooks[SlashContext]
"""Execution hooks for slash commands."""


class ExecutableCommand(abc.ABC, typing.Generic[ContextT_co]):
    """Base class for all commands that can be executed."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def checks(self) -> collections.Collection[CheckSig]:
        """Collection of checks that must be met before the command can be executed."""

    @property
    @abc.abstractmethod
    def component(self) -> typing.Optional[Component]:
        """Component that the command is registered with."""

    @property
    @abc.abstractmethod
    def hooks(self) -> typing.Optional[Hooks[ContextT_co]]:
        """Hooks that are triggered when the command is executed."""

    @property
    @abc.abstractmethod
    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
        """Mutable mapping of metadata set for this command.

        .. note::
            Any modifications made to this mutable mapping will be preserved by
            the command.
        """

    @abc.abstractmethod
    def bind_client(self: _T, client: Client, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def bind_component(self: _T, component: Component, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def copy(self: _T) -> _T:
        """Create a copy of this command.

        Returns
        -------
        Self
            A copy of this command.
        """

    @abc.abstractmethod
    def set_hooks(self: _T, _: typing.Optional[Hooks[ContextT_co]], /) -> _T:
        """Set the hooks that are triggered when the command is executed.

        Parameters
        ----------
        hooks : typing.Optional[Hooks[ContextT_co]]
            The hooks that are triggered when the command is executed.

        Returns
        -------
        Self
            This command to enable chained calls
        """

    @abc.abstractmethod
    def add_check(self: _T, check: CheckSig, /) -> _T:  # TODO: remove or add with_check?
        """Add a check to the command.

        Parameters
        ----------
        check : CheckSig
            The check to add.

        Returns
        -------
        Self
            This command to enable chained calls
        """

    @abc.abstractmethod
    def remove_check(self: _T, check: CheckSig, /) -> _T:
        """Remove a check from the command.

        Parameters
        ----------
        check : CheckSig
            The check to remove.

        Raises
        ------
        ValueError
            If the provided check isn't found.

        Returns
        -------
        Self
            This command to enable chained calls
        """

    @abc.abstractmethod
    def set_metadata(self: _T, key: typing.Any, value: typing.Any, /) -> _T:
        """Set a field in the command's metadata.

        Parameters
        ----------
        key : typing.Any
            Metadata key to set.
        value : typing.Any
            Metadata value to set.

        Returns
        -------
        Self
            The command instance to enable chained calls.
        """


class BaseSlashCommand(ExecutableCommand[SlashContext], abc.ABC):
    """Base class for all slash command classes."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def defaults_to_ephemeral(self) -> typing.Optional[bool]:
        """Whether contexts executed by this command should default to ephemeral responses.

        This effects calls to `SlashContext.create_followup`,
        `SlashContext.create_initial_response`, `SlashContext.defer` and
        `SlashContext.respond` unless the `flags` field is provided for the
        methods which support it.

        Returns
        -------
        bool
            Whether calls to this command should default to ephemeral mode.

            If this is `None` then the default from the parent command(s),
            component or client is used.
        """

    @property
    @abc.abstractmethod
    def is_global(self) -> bool:
        """Whether the command should be declared globally or not.

        .. warning::
            For commands within command groups the state of this flag
            is inherited regardless of what it's set as on the child command.
        """

    @property
    @abc.abstractmethod
    def name(self) -> str:
        """Name of the command."""

    @property
    @abc.abstractmethod
    def parent(self) -> typing.Optional[SlashCommandGroup]:
        """Object of the group this command is in."""

    @property
    def tracked_command(self) -> typing.Optional[hikari.Command]:
        """Object of the actual command this object tracks if set."""

    @property
    @abc.abstractmethod
    def tracked_command_id(self) -> typing.Optional[hikari.Snowflake]:
        """ID of the actual command this object tracks if set."""

    @abc.abstractmethod
    def build(self) -> hikari.api.CommandBuilder:
        """Get a builder object for this command.

        Returns
        -------
        hikari.api.CommandBuilder
            A builder object for this command. Use to declare this command on
            globally or for a specific guild.
        """

    @abc.abstractmethod
    async def check_context(self, ctx: SlashContext, /) -> bool:
        raise NotImplementedError

    @abc.abstractmethod
    async def execute(
        self,
        ctx: SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[SlashHooks]] = None,
    ) -> None:
        raise NotImplementedError

    @abc.abstractmethod
    def set_parent(self: _T, _: typing.Optional[SlashCommandGroup], /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def set_tracked_command(self: _T, command: hikari.Command, /) -> _T:
        """Set the global command this tracks.

        Parameters
        ----------
        command : hikari.Command
            Object of the global command this tracks.

        Returns
        -------
        Self
            The command instance to enable chained calls.
        """


class SlashCommand(BaseSlashCommand, abc.ABC, typing.Generic[CommandCallbackSigT]):
    """A command that can be executed in a slash context."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def callback(self) -> CommandCallbackSigT:
        """Callback which is called during execution."""


class SlashCommandGroup(BaseSlashCommand, abc.ABC):
    """Standard interface of a slash command group.

    .. note::
        Unlike `MessageCommandGroup`, slash command groups do not have
        their own callback.
    """

    __slots__ = ()

    @property
    @abc.abstractmethod
    def commands(self) -> collections.Collection[BaseSlashCommand]:
        """Collection of the commands in this group."""

    @abc.abstractmethod
    def add_command(self: _T, command: BaseSlashCommand, /) -> _T:
        """Add a command to this group.

        Parameters
        ----------
        command : BaseSlashCommand
            The command to add.

        Returns
        -------
        Self
            The command group instance to enable chained calls.
        """

    @abc.abstractmethod
    def remove_command(self: _T, command: BaseSlashCommand, /) -> _T:
        """Remove a command from this group.

        Parameters
        ----------
        command : BaseSlashCommand
            The command to remove.

        Raises
        ------
        ValueError
            If the provided command isn't found.

        Returns
        -------
        Self
            The command group instance to enable chained calls.
        """

    @abc.abstractmethod
    def with_command(self, command: BaseSlashCommandT, /) -> BaseSlashCommandT:
        """Add a command to this group through a decorator call.

        Parameters
        ----------
        command : BaseSlashCommand
            The command to add.

        Returns
        -------
        BaseSlashCommand
            The added command.
        """


class MessageParser(abc.ABC):
    """Base class for a message parser."""

    __slots__ = ()

    @abc.abstractmethod
    def bind_client(self: _T, client: Client, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def bind_component(self: _T, component: Component, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def copy(self: _T) -> _T:
        """Copy the parser.

        Returns
        -------
        Self
            A copy of the parser.
        """

    @abc.abstractmethod
    async def parse(self, ctx: MessageContext, /) -> dict[str, typing.Any]:
        """Parse a message context.

        .. warning::
            This relies on the prefix and command name(s) having been removed
            from `tanjun.abc.MessageContext.content`

        Parameters
        ----------
        ctx : tanjun.abc.MessageContext
            The message context to parse.

        Returns
        -------
        dict[str, typing.Any]
            Dictionary of argument names to the parsed values for them.

        Raises
        ------
        tanjun.errors.ParserError
            If the message could not be parsed.
        """


class MessageCommand(ExecutableCommand[MessageContext], abc.ABC, typing.Generic[CommandCallbackSigT]):
    """Standard interface of a message command."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def callback(self) -> CommandCallbackSigT:
        """Callback which is called during execution.

        .. note::
            For command groups, this is called when none of the inner-commands
            matches the message.
        """

    @property
    @abc.abstractmethod
    def names(self) -> collections.Collection[str]:
        """Collection of this command's names."""

    @property
    @abc.abstractmethod
    def parent(self) -> typing.Optional[MessageCommandGroup[typing.Any]]:
        """Parent group of this command if applicable."""

    @property
    @abc.abstractmethod
    def parser(self) -> typing.Optional[MessageParser]:
        """Parser for this command."""

    @abc.abstractmethod
    def set_parent(self: _T, _: typing.Optional[MessageCommandGroup[typing.Any]], /) -> _T:
        """Set the parent of this command.

        Parameters
        ----------
        parent : typing.Optional[MessageCommandGroup[typing.Any]]
            The parent of this command.

        Returns
        -------
        Self
            The command instance to enable chained calls.
        """

    @abc.abstractmethod
    def set_parser(self: _T, _: MessageParser, /) -> _T:
        """Set the for this message command.

        Parameters
        ----------
        parser : MessageParser
            The parser to set.

        Returns
        -------
        Self
            The command instance to enable chained calls.
        """

    @abc.abstractmethod
    def copy(self: _T, *, parent: typing.Optional[MessageCommandGroup[typing.Any]] = None) -> _T:
        """Create a copy of this command.

        Other Parameters
        ----------------
        parent : typing.Optional[MessageCommandGroup[tping.Any]]
            The parent of the copy.

        Returns
        -------
        Self
            The copy.
        """

    @abc.abstractmethod
    async def check_context(self, ctx: MessageContext, /) -> bool:
        raise NotImplementedError

    @abc.abstractmethod
    async def execute(
        self, ctx: MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[Hooks[MessageContext]]] = None
    ) -> None:
        raise NotImplementedError


class MessageCommandGroup(MessageCommand[CommandCallbackSigT], abc.ABC):
    """Standard interface of a message command group."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def commands(self) -> collections.Collection[MessageCommand[typing.Any]]:
        """Collection of the commands in this group.

        .. note::
            This may include command groups.
        """

    @abc.abstractmethod
    def add_command(self: _T, command: MessageCommand[typing.Any], /) -> _T:
        """Add a command to this group.

        Parameters
        ----------
        command : MessageCommand
            The command to add.

        Returns
        -------
        Self
            The group instance to enable chained calls.
        """

    @abc.abstractmethod
    def remove_command(self: _T, command: MessageCommand[typing.Any], /) -> _T:
        """Remove a command from this group.

        Parameters
        ----------
        command : MessageCommand
            The command to remove.

        Raises
        ------
        ValueError
            If the provided command isn't found.

        Returns
        -------
        Self
            The group instance to enable chained calls.
        """

    @abc.abstractmethod
    def with_command(self, command: MessageCommandT, /) -> MessageCommandT:
        """Add a command to this group through a decorator call.

        Parameters
        ----------
        command : MessageCommand
            The command to add.

        Returns
        -------
        MessageCommand
            The added command.
        """


class Component(abc.ABC):
    """Standard interface of a Tanjun component.

    This is a collection of message and slash commands, and listeners
    with logic for command search + execution and loading the listeners
    into a tanjun client.
    """

    __slots__ = ()

    @property
    @abc.abstractmethod
    def client(self) -> typing.Optional[Client]:
        """Tanjun client this component is bound to."""

    @property
    @abc.abstractmethod
    def defaults_to_ephemeral(self) -> typing.Optional[bool]:
        """Whether slash contexts executed in this component should default to ephemeral responses.

        This effects calls to `SlashContext.create_followup`,
        `SlashContext.create_initial_response`, `SlashContext.defer` and
        `SlashContext.respond` unless the `flags` field is provided for the
        methods which support it.

        Notes
        -----
        * This may be overridden by `BaseSlashCommand.defaults_to_ephemeral`.
        * This only effects slash command execution.
        * If this is `None` then the default from the parent client is used.
        """

    @property
    @abc.abstractmethod
    def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
        """The asyncio loop this client is bound to if it has been opened."""

    @property
    @abc.abstractmethod
    def name(self) -> str:
        """Component's unique identifier.

        .. note::
            This will be preserved between copies of a component.
        """

    @property
    @abc.abstractmethod
    def slash_commands(self) -> collections.Collection[BaseSlashCommand]:
        """Collection of the slash commands in this component."""

    @property
    @abc.abstractmethod
    def message_commands(self) -> collections.Collection[MessageCommand[typing.Any]]:
        """Collection of the message commands in this component."""

    @property
    @abc.abstractmethod
    def listeners(self) -> collections.Mapping[type[hikari.Event], collections.Collection[ListenerCallbackSig]]:
        """Mapping of event types to the listeners registered for them in this component."""

    @property
    @abc.abstractmethod
    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
        """Mutable mapping of the metadata set for this component.

        .. note::
            Any modifications made to this mutable mapping will be preserved by
            the component.
        """

    @abc.abstractmethod
    def set_metadata(self: _T, key: typing.Any, value: typing.Any, /) -> _T:
        """Set a field in the component's metadata.

        Parameters
        ----------
        key : typing.Any
            Metadata key to set.
        value : typing.Any
            Metadata value to set.

        Returns
        -------
        Self
            The component instance to enable chained calls.
        """

    @abc.abstractmethod
    def add_slash_command(self: _T, command: BaseSlashCommand, /) -> _T:
        """Add a slash command to this component.

        Parameters
        ----------
        command : BaseSlashCommand
            The command to add.

        Returns
        -------
        Self
            The component to enable chained calls.
        """

    @abc.abstractmethod
    def remove_slash_command(self: _T, command: BaseSlashCommand, /) -> _T:
        """Remove a slash command from this component.

        Parameters
        ----------
        command : BaseSlashCommand
            The command to remove.

        Raises
        ------
        ValueError
            If the provided command isn't found.

        Returns
        -------
        Self
            The component to enable chained calls.
        """

    @typing.overload
    @abc.abstractmethod
    def with_slash_command(self, command: BaseSlashCommandT, /) -> BaseSlashCommandT:
        ...

    @typing.overload
    @abc.abstractmethod
    def with_slash_command(
        self, /, *, copy: bool = False
    ) -> collections.Callable[[BaseSlashCommandT], BaseSlashCommandT]:
        ...

    @abc.abstractmethod
    def with_slash_command(
        self, command: BaseSlashCommandT = ..., /, *, copy: bool = False
    ) -> typing.Union[BaseSlashCommandT, collections.Callable[[BaseSlashCommandT], BaseSlashCommandT]]:
        """Add a slash command to this component through a decorator call.

        Parameters
        ----------
        command : BaseSlashCommandT
            The command to add.

        Other Parameters
        ----------------
        copy : bool
            Whether to copy the command before adding it.

        Returns
        -------
        BaseSlashCommandT
            The added command.
        """

    @abc.abstractmethod
    def add_message_command(self: _T, command: MessageCommand[typing.Any], /) -> _T:
        """Add a message command to this component.

        Parameters
        ----------
        command : MessageCommand[typing.Any]
            The command to add.

        Returns
        -------
        Self
            The component to enable chained calls.
        """

    @abc.abstractmethod
    def remove_message_command(self: _T, command: MessageCommand[typing.Any], /) -> _T:
        """Remove a message command from this component.

        Parameters
        ----------
        command : MessageCommand[typing.Any]
            The command to remove.

        Raises
        ------
        ValueError
            If the provided command isn't found.

        Returns
        -------
        Self
            The component to enable chained calls.
        """

    @typing.overload
    @abc.abstractmethod
    def with_message_command(self, command: MessageCommandT, /) -> MessageCommandT:
        ...

    @typing.overload
    @abc.abstractmethod
    def with_message_command(
        self, /, *, copy: bool = False
    ) -> collections.Callable[[MessageCommandT], MessageCommandT]:
        ...

    @abc.abstractmethod
    def with_message_command(
        self, command: MessageCommandT = ..., /, *, copy: bool = False
    ) -> typing.Union[MessageCommandT, collections.Callable[[MessageCommandT], MessageCommandT]]:
        """Add a message command to this component through a decorator call.

        Parameters
        ----------
        command : MessageCommandT
            The command to add.

        Other Parameters
        ----------------
        copy : bool
            Whether to copy the command before adding it.

        Returns
        -------
        MessageCommandT
            The added command.
        """

    @abc.abstractmethod
    def add_listener(self: _T, event: type[hikari.Event], listener: ListenerCallbackSig, /) -> _T:
        """Add a listener to this component.

        Parameters
        ----------
        event : type[hikari.Event]
            The event to listen for.
        listener : ListenerCallbackSig
            The listener to add.

        Returns
        -------
        Self
            The component to enable chained calls.
        """

    @abc.abstractmethod
    def remove_listener(self: _T, event: type[hikari.Event], listener: ListenerCallbackSig, /) -> _T:
        """Remove a listener from this component.

        Parameters
        ----------
        event : type[hikari.Event]
            The event to listen for.
        listener : ListenerCallbackSig
            The listener to remove.

        Raises
        ------
        ValueError
            If the listener is not registered for the provided event.

        Returns
        -------
        Self
            The component to enable chained calls.
        """

    # TODO: make event optional?
    @abc.abstractmethod
    def with_listener(
        self, event_type: type[hikari.Event]
    ) -> collections.Callable[[ListenerCallbackSigT], ListenerCallbackSigT,]:
        """Add a listener to this component through a decorator call.

        Parameters
        ----------
        event_type : type[hikari.Event]
            The event to listen for.

        Returns
        -------
        collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]
            Decorator callback which takes listener to add.
        """

    @abc.abstractmethod
    def bind_client(self: _T, client: Client, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def unbind_client(self: _T, client: Client, /) -> _T:
        raise NotImplementedError

    @abc.abstractmethod
    def check_message_name(self, name: str, /) -> collections.Iterator[tuple[str, MessageCommand[typing.Any]]]:
        """Check whether a name matches any of this component's registered message commands.

        Notes
        -----
        * This only checks for name matches against the top level command and
          will not account for sub-commands.
        * Dependent on implementation detail this may partial check name against
          command names using name.startswith(command_name), hence why it
          also returns the name a command was matched by.

        Parameters
        ----------
        name : str
            The name to check for command matches.

        Returns
        -------
        collections.abc.Iterator[tuple[str, MessageCommand[typing.Any]]]
            Iterator of tuples of command name matches to the relevant message
            command objects.
        """

    @abc.abstractmethod
    def check_slash_name(self, name: str, /) -> collections.Iterator[BaseSlashCommand]:
        """Check whether a name matches any of this component's registered slash commands.

        .. note::
            This won't check for sub-commands and will expect `name` to simply be
            the top level command name.

        Parameters
        ----------
        name : str
            The name to check for command matches.

        Returns
        -------
        collections.abc.Iterator[BaseSlashCommand]
            An iterator of the matching slash commands.
        """

    @abc.abstractmethod
    async def execute_interaction(
        self,
        ctx: SlashContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[SlashHooks]] = None,
    ) -> typing.Optional[collections.Awaitable[None]]:
        """Execute a slash context.

        .. note::
            Unlike `Component.execute_message`, this shouldn't be expected to
            raise `tanjun.errors.HaltExecution` nor `tanjun.errors.CommandError`.

        Parameters
        ----------
        ctx : SlashContext
            The context to execute.

        Other Parameters
        ----------------
        hooks : typing.Optional[collections.abc.MutableSet[SlashHooks]] = None
            Set of hooks to include in this command execution.

        Returns
        -------
        typing.Optional[collections.abc.Awaitable[None]]
            Awaitable used to wait for the command execution to finish.

            This may be awaited or left to run as a background task.

            If this is `None` then the client should carry on its search for a
            component with a matching command.
        """

    @abc.abstractmethod
    async def execute_message(
        self, ctx: MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[MessageHooks]] = None
    ) -> bool:
        """Execute a message context.

        Parameters
        ----------
        ctx : MessageContext
            The context to execute.

        Other Parameters
        ----------------
        hooks : typing.Optional[collections.abc.MutableSet[MessageHooks]] = None
            Set of hooks to include in this command execution.

        Returns
        -------
        bool
            Whether a message command was executed in this component with the
            provided context.

            If `False` then the client should carry on its search for a
            component with a matching command.

        Raises
        ------
        tanjun.errors.CommandError
            To end the command's execution with an error response message.
        tanjun.errors.HaltExecution
            To indicate that the client should stop searching for commands to
            execute with the current context.
        """

    @abc.abstractmethod
    async def close(self, *, unbind: bool = False) -> None:
        """Close the component.

        Other Parameters
        ----------------
        unbind : bool
            Whether to unbind from the client after this is closed.

            Defaults to `False`.

        Raises
        ------
        RuntimeError
            If the component isn't running.
        """

    @abc.abstractmethod
    async def open(self) -> None:
        """Start the component.

        Raises
        ------
        RuntimeError
            If the component is already open.
            If the component isn't bound to a client.
        """


class ClientCallbackNames(str, enum.Enum):
    """Enum of the standard client callback names.

    These should be dispatched by all `Client` implementations.
    """

    CLOSED = "closed"
    """Called when the client has finished closing.

    No positional arguments are provided for this event.
    """

    CLOSING = "closing"
    """Called when the client is initially instructed to close.

    No positional arguments are provided for this event.
    """

    COMPONENT_ADDED = "component_added"
    """Called when a component is added to an active client.

    .. warning::
        This event isn't dispatched for components which were registered while
        the client is inactive.

    The first positional argument is the `tanjun.abc.Component` being added.
    """

    COMPONENT_REMOVED = "component_removed"
    """Called when a component is added to an active client.

    .. warning::
        This event isn't dispatched for components which were removed while
        the client is inactive.

    The first positional argument is the `tanjun.abc.Component` being removed.
    """

    MESSAGE_COMMAND_NOT_FOUND = "message_command_not_found"
    """Called when a message command is not found.

    `tanjun.abc.MessageContext` is provided as the first positional argument.
    """

    SLASH_COMMAND_NOT_FOUND = "slash_command_not_found"
    """Called when a slash command is not found.

    `tanjun.abc.MessageContext` is provided as the first positional argument.
    """

    STARTED = "started"
    """Called when the client has finished starting.

    No positional arguments are provided for this event.
    """

    STARTING = "starting"
    """Called when the client is initially instructed to start.

    No positional arguments are provided for this event.
    """


class Client(abc.ABC):
    """Abstract interface of a Tanjun client.

    This should manage both message and slash command execution based on the
    provided hikari clients.
    """

    __slots__ = ()

    @property
    @abc.abstractmethod
    def cache(self) -> typing.Optional[hikari.api.Cache]:
        """Hikari cache instance this command client was initialised with."""

    @property
    @abc.abstractmethod
    def components(self) -> collections.Collection[Component]:
        """Collection of the components this command client is using."""

    @property
    @abc.abstractmethod
    def defaults_to_ephemeral(self) -> bool:
        """Whether slash contexts spawned by this client should default to ephemeral responses.

        This effects calls to `SlashContext.create_followup`,
        `SlashContext.create_initial_response`, `SlashContext.defer` and
        `SlashContext.respond` unless the `flags` field is provided for the
        methods which support it.

        Notes
        -----
        * This may be overridden by `BaseSlashCommand.defaults_to_ephemeral`
          and `Component.defaults_to_ephemeral`.
        * This defaults to `False`.
        * This only effects slash command execution.
        """

    @property
    @abc.abstractmethod
    def events(self) -> typing.Optional[hikari.api.EventManager]:
        """Object of the event manager this client was initialised with.

        This is used for executing message commands if set.
        """

    @property
    @abc.abstractmethod
    def is_alive(self) -> bool:
        """Whether this client is alive."""

    @property  # TODO: switch over to a mapping of event to collection cause convenience
    @abc.abstractmethod
    def listeners(self) -> collections.Mapping[type[hikari.Event], collections.Collection[ListenerCallbackSig]]:
        """Mapping of event types to the listeners registered in this client."""

    @property
    @abc.abstractmethod
    def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
        """The loop this client is bound to if it's alive."""

    @property
    @abc.abstractmethod
    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
        """Mutable mapping of the metadata set for this client.

        .. note::
            Any modifications made to this mutable mapping will be preserved by
            the client.
        """

    @property
    @abc.abstractmethod
    def prefixes(self) -> collections.Collection[str]:
        """Collection of the prefixes set for this client.

        These are only use during message command execution to match commands
        to this command client.
        """

    @property
    @abc.abstractmethod
    def rest(self) -> hikari.api.RESTClient:
        """Object of the Hikari REST client this client was initialised with."""

    @property
    @abc.abstractmethod
    def server(self) -> typing.Optional[hikari.api.InteractionServer]:
        """Object of the Hikari interaction server provided for this client.

        This is used for executing slash commands if set.
        """

    @property
    @abc.abstractmethod
    def shards(self) -> typing.Optional[hikari_traits.ShardAware]:
        """Object of the Hikari shard manager this client was initialised with."""

    @property
    def voice(self) -> typing.Optional[hikari.api.VoiceComponent]:
        """Object of the Hikari voice component this client was initialised with."""

    @abc.abstractmethod
    async def clear_application_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> None:
        """Clear the commands declared either globally or for a specific guild.

        .. note::
            The endpoint this uses has a strict ratelimit which, as of writing,
            only allows for 2 requests per minute (with that ratelimit either
            being per-guild if targeting a specific guild otherwise globally).

        Other Parameters
        ----------------
        application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]
            The application to clear commands for.

            If left as `None` then this will be inferred from the authorization
            being used by `Client.rest`.
        guild : hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]
            Object or ID of the guild to clear commands for.

            If left as `None` global commands will be cleared.
        """

    @abc.abstractmethod
    async def declare_global_commands(
        self,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        """Set the global application commands for a bot based on the loaded components.

        .. warning::
            This will overwrite any previously set application commands and
            only targets commands marked as global.

        Notes
        -----
        * The endpoint this uses has a strict ratelimit which, as of writing,
          only allows for 2 requests per minute (with that ratelimit either
          being per-guild if targeting a specific guild otherwise globally).
        * Setting a specific `guild` can be useful for testing/debug purposes
          as slash commands may take up to an hour to propagate globally but
          will immediately propagate when set on a specific guild.

        Other Parameters
        ----------------
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the existing commands to update.
        application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]
            Object or ID of the application to set the global commands for.

            If left as `None` then this will be inferred from the authorization
            being used by `Client.rest`.
        guild : hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]
            Object or ID of the guild to set the global commands to.

            If left as `None` global commands will be set.
        force : bool
            Force this to declare the commands regardless of whether or not
            they match the current state of the declared commands.

            Defaults to `False`. This default behaviour helps avoid issues with the
            2 request per minute (per-guild or globally) ratelimit and the other limit
            of only 200 application command creates per day (per guild or globally).

        Returns
        -------
        collections.abc.Sequence[hikari..Command]
            API representations of the set commands.
        """

    @abc.abstractmethod
    async def declare_application_command(
        self,
        command: BaseSlashCommand,
        /,
        command_id: typing.Optional[hikari.Snowflakeish] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> hikari.Command:
        """Declare a single slash command for a bot.

        .. warning::
            Providing `command_id` when updating a command helps avoid any
            permissions set for the command being lose (e.g. when changing the
            command's name).

        Parameters
        ----------
        command : BaseSlashCommand
            The command to register.

        Other Parameters
        ----------------
        application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]
            The application to register the command with.

            If left as `None` then this will be inferred from the authorization
            being used by `Client.rest`.
        command_id : typing.Optional[hikari.snowflakes.Snowflakeish]
            ID of the command to update.
        guild : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]
            Object or ID of the guild to register the command with.

            If left as `None` then the command will be registered globally.

        Returns
        -------
        hikari.Command
            API representation of the command that was registered.
        """

    @abc.abstractmethod
    async def declare_application_commands(
        self,
        commands: collections.Iterable[BaseSlashCommand],
        /,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        """Declare a collection of slash commands for a bot.

        .. note::
            The endpoint this uses has a strict ratelimit which, as of writing,
            only allows for 2 requests per minute (with that ratelimit either
            being per-guild if targeting a specific guild otherwise globally).

        Parameters
        ----------
        commands : collections.abc.Iterable[BaseSlashCommand]
            Iterable of the commands to register.

        Other Parameters
        ----------------
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the existing commands to update.

            While optional, this can be helpful when updating commands as
            providing the current IDs will prevent changes such as renames from
            leading to other state set for commands (e.g. permissions) from
            being lost.
        application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]
            The application to register the commands with.

            If left as `None` then this will be inferred from the authorization
            being used by `Client.rest`.
        guild : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]
            Object or ID of the guild to register the commands with.

            If left as `None` then the commands will be registered globally.
        force : bool
            Force this to declare the commands regardless of whether or not
            they match the current state of the declared commands.

            Defaults to `False`. This default behaviour helps avoid issues with the
            2 request per minute (per-guild or globally) ratelimit and the other limit
            of only 200 application command creates per day (per guild or globally).

        Returns
        -------
        collections.abc.Sequence[hikari.Command]
            API representations of the commands which were registered.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If conflicting command names are found (multiple commanbds have the same top-level name).
            * If more than 100 top-level commands are passed.
        """

    @abc.abstractmethod
    def set_metadata(self: _T, key: typing.Any, value: typing.Any, /) -> _T:
        """Set a field in the client's metadata.

        Parameters
        ----------
        key : typing.Any
            Metadata key to set.
        value : typing.Any
            Metadata value to set.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """

    @abc.abstractmethod
    def add_component(self: _T, component: Component, /) -> _T:
        """Add a component to this client.

        Parameters
        ----------
        component: Component
            The component to move to this client.

        Returns
        -------
        Self
            The client instance to allow chained calls.
        """

    @abc.abstractmethod
    def get_component_by_name(self, name: str, /) -> typing.Optional[Component]:
        """Get a component from this client by name.

        Parameters
        ----------
        name : str
            Name to get a component by.

        Returns
        -------
        typing.Optional[Component]
            The component instance if found, else `None`.
        """

    @abc.abstractmethod
    def remove_component(self: _T, component: Component, /) -> _T:
        """Remove a component from this client.

        This will unsubscribe any client callbacks, commands and listeners
        registered in the provided component.

        Parameters
        ----------
        component: Component
            The component to remove from this client.

        Raises
        ------
        ValueError
            If the provided component isn't found.

        Returns
        -------
        Self
            The client instance to allow chained calls.
        """

    @abc.abstractmethod
    def remove_component_by_name(self: _T, name: str, /) -> _T:
        """Remove a component from this client by name.

        This will unsubscribe any client callbacks, commands and listeners
        registered in the provided component.

        Parameters
        ----------
        name: str
            Name of the component to remove from this client.

        Raises
        ------
        KeyError
            If the provided component name isn't found.
        """

    @abc.abstractmethod
    def add_client_callback(self: _T, name: typing.Union[str, ClientCallbackNames], callback: MetaEventSig, /) -> _T:
        """Add a client callback.

        Parameters
        ----------
        name : typing.Union[str, ClientCallbackNames]
            The name this callback is being registered to.

            This is case-insensitive.
        callback : MetaEventSigT
            The callback to register.

            This may be sync or async and must return None. The positional and
            keyword arguments a callback should expect depend on implementation
            detail around the `name` being subscribed to.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """

    @abc.abstractmethod
    async def dispatch_client_callback(
        self, name: typing.Union[str, ClientCallbackNames], /, *args: typing.Any
    ) -> None:
        """Dispatch a client callback.

        Parameters
        ----------
        name : typing.Union[str, ClientCallbackNames]
            The name of the callback to dispatch.

        Other Parameters
        ----------------
        *args : typing.Any
            Positional arguments to pass to the callback(s).

        Raises
        ------
        KeyError
            If no callbacks are registered for the given name.
        """

    @abc.abstractmethod
    def get_client_callbacks(
        self, name: typing.Union[str, ClientCallbackNames], /
    ) -> collections.Collection[MetaEventSig]:
        """Get a collection of the callbacks registered for a specific name.

        Parameters
        ----------
        name : typing.Union[str, ClientCallbackNames]
            The name to get the callbacks registered for.

            This is case-insensitive.

        Returns
        -------
        collections.abc.Collection[MetaEventSig]
            Collection of the callbacks for the provided name.
        """

    @abc.abstractmethod
    def remove_client_callback(self: _T, name: typing.Union[str, ClientCallbackNames], callback: MetaEventSig, /) -> _T:
        """Remove a client callback.

        Parameters
        ----------
        name : typing.Union[str, ClientCallbackNames]
            The name this callback is being registered to.

            This is case-insensitive.
        callback : MetaEventSigT
            The callback to remove from the client's callbacks.

        Raises
        ------
        KeyError
            If the provided name isn't found.
        ValueError
            If the provided callback isn't found.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """

    @abc.abstractmethod
    def with_client_callback(
        self, name: typing.Union[str, ClientCallbackNames], /
    ) -> collections.Callable[[MetaEventSigT], MetaEventSigT]:
        """Add a client callback through a decorator call.

        Examples
        --------
        ```py
        client = tanjun.Client.from_rest_bot(bot)

        @client.with_client_callback("closed")
        async def on_close() -> None:
            raise NotImplementedError
        ```

        Parameters
        ----------
        name : typing.Union[str, ClientCallbackNames]
            The name this callback is being registered to.

            This is case-insensitive.

        Returns
        -------
        collections.abc.Callable[[MetaEventSigT], MetaEventSigT]
            Decorator callback used to register the client callback.

            This may be sync or async and must return None. The positional and
            keyword arguments a callback should expect depend on implementation
            detail around the `name` being subscribed to.
        """

    @abc.abstractmethod
    def add_listener(self: _T, event_type: type[hikari.Event], callback: ListenerCallbackSig, /) -> _T:
        """Add a listener to the client.

        Parameters
        ----------
        event_type : type[hikari.Event]
            The event type to add a listener for.
        callback: ListenerCallbackSig
            The callback to register as a listener.

            This callback must be a coroutine function which returns `None` and
            always takes at least one positional arg of type `hikari.Event`
            regardless of client implementation detail.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """

    @abc.abstractmethod
    def remove_listener(self: _T, event_type: type[hikari.Event], callback: ListenerCallbackSig, /) -> _T:
        """Remove a listener from the client.

        Parameters
        ----------
        event_type : type[hikari.Event]
            The event type to remove a listener for.
        callback: ListenerCallbackSig
            The callback to remove.

        Raises
        ------
        KeyError
            If the provided event type isn't found.
        ValueError
            If the provided callback isn't found.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """

    @abc.abstractmethod
    def with_listener(
        self, event_type: type[hikari.Event], /
    ) -> collections.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]:
        """Add an event listener to this client through a decorator call.

        Examples
        --------
        ```py
        client = tanjun.Client.from_gateway_bot(bot)

        @client.with_listener(hikari.MessageCreateEvent)
        async def on_message_create(event: hikari.MessageCreateEvent) -> None:
            raise NotImplementedError
        ```

        Parameters
        ----------
        event_type : type[hikari.Event]
            The event type to listener for.

        Returns
        -------
        collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]
            Decorator callback used to register the event callback.

            The callback must be a coroutine function which returns `None` and
            always takes at least one positional arg of type `hikari.Event`
            regardless of client implementation detail.
        """

    @abc.abstractmethod
    def iter_commands(self) -> collections.Iterator[ExecutableCommand[Context]]:
        """Iterate over all the commands (both message and slash) registered to this client.

        Returns
        -------
        collections.abc.Iterator[ExecutableCommand[Context]]
            Iterator of all the commands registered to this client.
        """

    @abc.abstractmethod
    def iter_message_commands(self) -> collections.Iterator[MessageCommand[typing.Any]]:
        """Iterate over all the message commands registered to this client.

        Returns
        -------
        collections.abc.Iterator[MessageCommand]
            Iterator of all the message commands registered to this client.
        """

    @abc.abstractmethod
    def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[BaseSlashCommand]:
        """Iterate over all the slash commands registered to this client.

        Parameters
        ----------
        global_only : bool
            Whether to only iterate over global slash commands.

        Returns
        -------
        collections.abc.Iterator[BaseSlashCommand]
            Iterator of all the slash commands registered to this client.
        """

    @abc.abstractmethod
    def check_message_name(self, name: str, /) -> collections.Iterator[tuple[str, MessageCommand[typing.Any]]]:
        """Check whether a message command name is present in the current client.

        .. note::
            Dependent on implementation this may partial check name against the
            message command's name based on command_name.startswith(name).

        Parameters
        ----------
        name : str
            The name to match commands against.

        Returns
        -------
        collections.abc.Iterator[tuple[str, MessageCommand]]
            Iterator of the matched command names to the matched message command objects.
        """

    @abc.abstractmethod
    def check_slash_name(self, name: str, /) -> collections.Iterator[BaseSlashCommand]:
        """Check whether a slash command name is present in the current client.

        .. note::
            This won't check the commands within command groups.

        Parameters
        ----------
        name : str
            Name to check against.

        Returns
        -------
        collections.abc.Iterator[BaseSlashCommand]
            Iterator of the matched slash command objects.
        """

    @abc.abstractmethod
    def load_modules(self: _T, *modules: typing.Union[str, pathlib.Path]) -> _T:
        """Load entities into this client from modules based on present loaders.

        .. note::
            If an `__all__` is present in the target module then it will be
            used to find loaders.

        Examples
        --------
        For this to work the target module has to have at least one loader present.

        ```py
        @tanjun.as_loader
        def load_module(client: tanjun.Client) -> None:
            client.add_component(component.copy())
        ```

        or

        ```py
        loader = tanjun.Component("trans component").load_from_scope().make_loader()
        ```

        Parameters
        ----------
        *modules : typing.Union[str, pathlib.Path]
            Path(s) of the modules to load from.

            When `str` this will be treated as a normal import path which is
            absolute (`"foo.bar.baz"`). It's worth noting that absolute module
            paths may be imported from the current location if the top level
            module is a valid module file or module directory in the current
            working directory.

            When `pathlib.Path` the module will be imported directly from
            the given path. In this mode any relative imports in the target
            module will fail to resolve.

        Returns
        -------
        Self
            This client instance to enable chained calls.

        Raises
        ------
        tanjun.errors.FailedModuleLoad
            If the new version of a module failed to load.

            This includes if it failed to import or if one of its loaders raised.
            The source error can be found at `tanjun.errors.FailedModuleLoad.__source__`.
        tanjun.errors.ModuleStateConflict
            If the module is already loaded.
        tanjun.errors.ModuleMissingLoaders
            If no loaders are found in the module.
        ModuleNotFoundError
            If the module is not found.
        """

    @abc.abstractmethod
    async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        """Asynchronous variant of `Client.load_modules`.

        Unlike `Client.load_modules`, this method will run blocking code in a
        background thread.

        For more information on the behaviour of this method see the
        documentation for `Client.load_modules`.
        """

    @abc.abstractmethod
    def unload_modules(self: _T, *modules: typing.Union[str, pathlib.Path]) -> _T:
        """Unload entities from this client based on unloaders in one or more modules.

        .. note::
            If an `__all__` is present in the target module then it will be
            used to find unloaders.

        Examples
        --------
        For this to work the module has to have at least one unloading enabled
        `tanjun.abc.ClientLoader` present.

        ```py
        @tanjun.as_unloader
        def unload_component(client: tanjun.Client) -> None:
            client.remove_component_by_name(component.name)
        ```

        or

        ```py
        # as_loader's returned ClientLoader handles both loading and unloading.
        loader = tanjun.Component("trans component").load_from_scope().as_loader(unload_component)
        ```

        Parameters
        ----------
        *modules: typing.Union[str, pathlib.Path]
            Path of one or more modules to unload.

            These should be the same path(s) which were passed to `load_module`.

        Returns
        -------
        Self
            This client instance to enable chained calls.

        Raises
        ------
        tanjun.errors.ModuleStateConflict
            If the module hasn't been loaded.
        tanjun.errors.ModuleMissingLoaders
            If no unloaders are found in the module.
        tanjun.errors.FailedModuleUnload
            If the old version of a module failed to unload.

            This indicates that one of its unloaders raised. The source
            error can be found at `tanjun.errors.FailedModuleUnload.__source__`.
        """

    @abc.abstractmethod
    def reload_modules(self: _T, *modules: typing.Union[str, pathlib.Path]) -> _T:
        """Reload entities in this client based on the loaders in loaded module(s).

        .. note::
            If an `__all__` is present in the target module then it will be
            used to find loaders and unloaders.

        Examples
        --------
        For this to work the module has to have at least one ClientLoader
        which handles loading and one which handles unloading present.

        Parameters
        ----------
        *modules: typing.Union[str, pathlib.Path]
            Paths of one or more module to unload.

            These should be the same paths which were passed to `load_module`.

        Returns
        -------
        Self
            This client instance to enable chained calls.

        Raises
        ------
        tanjun.errors.FailedModuleLoad
            If the new version of a module failed to load.

            This includes if it failed to import or if one of its loaders raised.
            The source error can be found at `tanjun.errors.FailedModuleLoad.__source__`.
        tanjun.errors.FailedModuleUnload
            If the old version of a module failed to unload.

            This indicates that one of its unloaders raised. The source
            error can be found at `tanjun.errors.FailedModuleUnload.__source__`.
        tanjun.errors.ModuleStateConflict
            If the module hasn't been loaded.
        tanjun.errors.ModuleMissingLoaders
            If no unloaders are found in the current state of the module.
            If no loaders are found in the new state of the module.
        ModuleNotFoundError
            If the module can no-longer be found at the provided path.
        """

    @abc.abstractmethod
    async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        """Asynchronous variant of `Client.reload_modules`.

        Unlike `Client.reload_modules`, this method will run blocking code in a
        background thread.

        For more information on the behaviour of this method see the
        documentation for `Client.reload_modules`.
        """


class ClientLoader(abc.ABC):
    """Interface of logic used to load and unload components into a generic client."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def has_load(self) -> bool:
        """Whether this loader will load anything."""

    @property
    @abc.abstractmethod
    def has_unload(self) -> bool:
        """Whether this loader will unload anything."""

    @abc.abstractmethod
    def load(self, client: Client, /) -> bool:
        """Load logic into a client instance.

        Parameters
        ----------
        client : Client
            The client to load commands and listeners for.

        Returns
        -------
        bool
            Whether anything was loaded.
        """

    @abc.abstractmethod
    def unload(self, client: Client, /) -> bool:
        """Unload logic from a client instance.

        Parameters
        ----------
        client : Client
            The client to unload commands and listeners from.

        Returns
        -------
        bool
            Whether anything was unloaded.
        """

Interfaces of the objects and clients used within Tanjun.

#   class ClientCallbackNames(builtins.str, enum.Enum):
View Source
class ClientCallbackNames(str, enum.Enum):
    """Enum of the standard client callback names.

    These should be dispatched by all `Client` implementations.
    """

    CLOSED = "closed"
    """Called when the client has finished closing.

    No positional arguments are provided for this event.
    """

    CLOSING = "closing"
    """Called when the client is initially instructed to close.

    No positional arguments are provided for this event.
    """

    COMPONENT_ADDED = "component_added"
    """Called when a component is added to an active client.

    .. warning::
        This event isn't dispatched for components which were registered while
        the client is inactive.

    The first positional argument is the `tanjun.abc.Component` being added.
    """

    COMPONENT_REMOVED = "component_removed"
    """Called when a component is added to an active client.

    .. warning::
        This event isn't dispatched for components which were removed while
        the client is inactive.

    The first positional argument is the `tanjun.abc.Component` being removed.
    """

    MESSAGE_COMMAND_NOT_FOUND = "message_command_not_found"
    """Called when a message command is not found.

    `tanjun.abc.MessageContext` is provided as the first positional argument.
    """

    SLASH_COMMAND_NOT_FOUND = "slash_command_not_found"
    """Called when a slash command is not found.

    `tanjun.abc.MessageContext` is provided as the first positional argument.
    """

    STARTED = "started"
    """Called when the client has finished starting.

    No positional arguments are provided for this event.
    """

    STARTING = "starting"
    """Called when the client is initially instructed to start.

    No positional arguments are provided for this event.
    """

Enum of the standard client callback names.

These should be dispatched by all Client implementations.

#   CLOSED = <ClientCallbackNames.CLOSED: 'closed'>

Called when the client has finished closing.

No positional arguments are provided for this event.

#   CLOSING = <ClientCallbackNames.CLOSING: 'closing'>

Called when the client is initially instructed to close.

No positional arguments are provided for this event.

#   COMPONENT_ADDED = <ClientCallbackNames.COMPONENT_ADDED: 'component_added'>

Called when a component is added to an active client.

Warning: This event isn't dispatched for components which were registered while the client is inactive.

The first positional argument is the tanjun.abc.Component being added.

#   COMPONENT_REMOVED = <ClientCallbackNames.COMPONENT_REMOVED: 'component_removed'>

Called when a component is added to an active client.

Warning: This event isn't dispatched for components which were removed while the client is inactive.

The first positional argument is the tanjun.abc.Component being removed.

#   MESSAGE_COMMAND_NOT_FOUND = <ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND: 'message_command_not_found'>

Called when a message command is not found.

tanjun.abc.MessageContext is provided as the first positional argument.

#   SLASH_COMMAND_NOT_FOUND = <ClientCallbackNames.SLASH_COMMAND_NOT_FOUND: 'slash_command_not_found'>

Called when a slash command is not found.

tanjun.abc.MessageContext is provided as the first positional argument.

#   STARTED = <ClientCallbackNames.STARTED: 'started'>

Called when the client has finished starting.

No positional arguments are provided for this event.

#   STARTING = <ClientCallbackNames.STARTING: 'starting'>

Called when the client is initially instructed to start.

No positional arguments are provided for this event.

Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""A collection of common standard checks designed for Tanjun commands."""

from __future__ import annotations

__all__: list[str] = [
    "all_checks",
    "any_checks",
    "CallbackReturnT",
    "CommandT",
    "with_all_checks",
    "with_any_checks",
    "with_check",
    "with_dm_check",
    "with_guild_check",
    "with_nsfw_check",
    "with_sfw_check",
    "with_owner_check",
    "with_author_permission_check",
    "with_own_permission_check",
    "DmCheck",
    "GuildCheck",
    "NsfwCheck",
    "SfwCheck",
    "OwnerCheck",
    "AuthorPermissionCheck",
    "OwnPermissionCheck",
]

import typing
from collections import abc as collections

import hikari

from . import dependencies
from . import errors
from . import injecting
from . import utilities

if typing.TYPE_CHECKING:
    from . import abc as tanjun_abc


CommandT = typing.TypeVar("CommandT", bound="tanjun_abc.ExecutableCommand[typing.Any]")
# This errors on earlier 3.9 releases when not quotes cause dumb handling of the [CommandT] list
CallbackReturnT = typing.Union[CommandT, "collections.Callable[[CommandT], CommandT]"]
"""Type hint for the return value of decorators which optionally take keyword arguments.

Examples
--------
Decorator functions with this as their return type may either be used as a
decorator directly without being explicitly called:

```python
@with_dm_check
@as_command("foo")
def foo_command(self, ctx: Context) -> None:
    raise NotImplemented
```

Or may be called with the listed other parameters as keyword arguments
while decorating a function.

```python
@with_dm_check(halt_execution=True)
@as_command("foo")
def foo_command(self, ctx: Context) -> None:
    raise NotImplemented
```
"""


class InjectableCheck(injecting.CallbackDescriptor[bool]):
    __slots__ = ()

    async def __call__(self, ctx: tanjun_abc.Context, /) -> bool:
        if result := await self.resolve_with_command_context(ctx, ctx):
            return result

        raise errors.FailedCheck


def _optional_kwargs(
    command: typing.Optional[CommandT], check: tanjun_abc.CheckSig, /
) -> typing.Union[CommandT, collections.Callable[[CommandT], CommandT]]:
    if command:
        return command.add_check(check)

    return lambda c: c.add_check(check)


class _Check:
    __slots__ = ("_error_message", "_halt_execution")

    def __init__(
        self,
        error_message: typing.Optional[str],
        halt_execution: bool,
    ) -> None:
        self._error_message = error_message
        self._halt_execution = halt_execution

    def _handle_result(self, result: bool) -> bool:
        if not result:
            if self._error_message:
                raise errors.CommandError(self._error_message) from None
            if self._halt_execution:
                raise errors.HaltExecution from None

        return result


class OwnerCheck(_Check):
    """Standard owner check callback registered by `with_owner_check`.

    This check will only pass if the author of the command is a bot owner.
    """

    __slots__ = ()

    def __init__(
        self,
        *,
        error_message: typing.Optional[str] = "Only bot owners can use this command",
        halt_execution: bool = False,
    ) -> None:
        """Initialise a owner check.

        .. note::
            error_message takes priority over halt_execution.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "Only bot owners can use this command" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message, halt_execution)

    async def __call__(
        self,
        ctx: tanjun_abc.Context,
        dependency: dependencies.AbstractOwners = injecting.inject(type=dependencies.AbstractOwners),
    ) -> bool:
        return self._handle_result(await dependency.check_ownership(ctx.client, ctx.author))


_GuildChannelCacheT = typing.Optional[dependencies.SfCache[hikari.GuildChannel]]


async def _get_is_nsfw(
    ctx: tanjun_abc.Context,
    /,
    *,
    dm_default: bool,
    channel_cache: _GuildChannelCacheT,
) -> bool:
    if ctx.guild_id is None:
        return dm_default

    channel: typing.Optional[hikari.PartialChannel] = None
    if ctx.cache and (channel := ctx.cache.get_guild_channel(ctx.channel_id)):
        return channel.is_nsfw or False

    if channel_cache:
        try:
            return (await channel_cache.get(ctx.channel_id)).is_nsfw or False

        except dependencies.EntryNotFound:
            raise

        except dependencies.CacheMissError:
            pass

    channel = await ctx.rest.fetch_channel(ctx.channel_id)
    assert isinstance(channel, hikari.GuildChannel)
    return channel.is_nsfw or False


class NsfwCheck(_Check):
    """Standard NSFW check callback registered by `with_nsfw_check`.

    This check will only pass if the current channel is NSFW.
    """

    __slots__ = ()

    def __init__(
        self,
        *,
        error_message: typing.Optional[str] = "Command can only be used in NSFW channels",
        halt_execution: bool = False,
    ) -> None:
        """Initialise a NSFW check.

        .. note::
            error_message takes priority over halt_execution.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "Command can only be used in NSFW channels" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message, halt_execution)

    async def __call__(
        self,
        ctx: tanjun_abc.Context,
        /,
        channel_cache: _GuildChannelCacheT = injecting.inject(type=_GuildChannelCacheT),
    ) -> bool:
        return self._handle_result(await _get_is_nsfw(ctx, dm_default=True, channel_cache=channel_cache))


class SfwCheck(_Check):
    """Standard SFW check callback registered by `with_sfw_check`.

    This check will only pass if the current channel is SFW.
    """

    __slots__ = ()

    def __init__(
        self,
        *,
        error_message: typing.Optional[str] = "Command can only be used in SFW channels",
        halt_execution: bool = False,
    ) -> None:
        """Initialise a SFW check.

        .. note::
            error_message takes priority over halt_execution.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "Command can only be used in SFW channels" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message, halt_execution)

    async def __call__(
        self,
        ctx: tanjun_abc.Context,
        /,
        channel_cache: _GuildChannelCacheT = injecting.inject(type=_GuildChannelCacheT),
    ) -> bool:
        return self._handle_result(not await _get_is_nsfw(ctx, dm_default=False, channel_cache=channel_cache))


class DmCheck(_Check):
    """Standard DM check callback registered by `with_dm_check`.

    This check will only pass if the current channel is a DM channel.
    """

    __slots__ = ()

    def __init__(
        self,
        *,
        error_message: typing.Optional[str] = "Command can only be used in DMs",
        halt_execution: bool = False,
    ) -> None:
        """Initialise a DM check.

        .. note::
            error_message takes priority over halt_execution.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "Command can only be used in DMs" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message, halt_execution)

    def __call__(self, ctx: tanjun_abc.Context, /) -> bool:
        return self._handle_result(ctx.guild_id is None)


class GuildCheck(_Check):
    """Standard guild check callback registered by `with_guild_check`.

    This check will only pass if the current channel is in a guild.
    """

    __slots__ = ()

    def __init__(
        self,
        *,
        error_message: typing.Optional[str] = "Command can only be used in guild channels",
        halt_execution: bool = False,
    ) -> None:
        """Initialise a guild check.

        .. note::
            error_message takes priority over halt_execution.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "Command can only be used in guild channels" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message, halt_execution)

    def __call__(self, ctx: tanjun_abc.Context, /) -> bool:
        return self._handle_result(ctx.guild_id is not None)


class AuthorPermissionCheck(_Check):
    """Standard author permission check callback registered by `with_author_permission_check`.

    This check will only pass if the current author has the specified permission.
    """

    __slots__ = ("_permissions",)

    def __init__(
        self,
        permissions: typing.Union[hikari.Permissions, int],
        /,
        *,
        error_message: typing.Optional[str] = "You don't have the permissions required to use this command",
        halt_execution: bool = False,
    ) -> None:
        """Initialise an author permission check.

        .. note::
            error_message takes priority over halt_execution.

        Parameters
        ----------
        permissions: typing.Union[hikari.permissions.Permissions, int]
            The permission(s) required for this command to run.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "You don't have the permissions required to use this command" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message=error_message, halt_execution=halt_execution)
        self._permissions = permissions

    async def __call__(self, ctx: tanjun_abc.Context, /) -> bool:
        if not ctx.member:
            # If there's no member when this is within a guild then it's likely
            # something like a webhook or guild visitor with no real permissions
            # outside of some basic set of send messages.
            if ctx.guild_id:
                permissions = await utilities.fetch_everyone_permissions(
                    ctx.client, ctx.guild_id, channel=ctx.channel_id
                )

            else:
                permissions = utilities.DM_PERMISSIONS

        elif isinstance(ctx.member, hikari.InteractionMember):
            permissions = ctx.member.permissions

        else:
            permissions = await utilities.fetch_permissions(ctx.client, ctx.member, channel=ctx.channel_id)

        return self._handle_result((self._permissions & permissions) == self._permissions)


class OwnPermissionCheck(_Check):
    """Standard own permission check callback registered by `with_own_permission_check`.

    This check will only pass if the current bot user has the specified permission.
    """

    __slots__ = ("_permissions",)

    def __init__(
        self,
        permissions: typing.Union[hikari.Permissions, int],
        /,
        *,
        error_message: typing.Optional[str] = "Bot doesn't have the permissions required to run this command",
        halt_execution: bool = False,
    ) -> None:
        """Initialise a own permission check.

        .. note::
            error_message takes priority over halt_execution.

        Parameters
        ----------
        permissions: typing.Union[hikari.permissions.Permissions, int]
            The permission(s) required for this command to run.

        Other Parameters
        ----------------
        error_message : typing.Optional[str]
            The error message to send in response as a command error if the check fails.

            Defaults to "Bot doesn't have the permissions required to run this command" and setting this to `None`
            will disable the error message allowing the command search to continue.
        halt_execution : bool
            Whether this check should raise `tanjun.errors.HaltExecution` to
            end the execution search when it fails instead of returning `False`.

            Defaults to `False`.
        """
        super().__init__(error_message=error_message, halt_execution=halt_execution)
        self._permissions = permissions

    async def __call__(
        self,
        ctx: tanjun_abc.Context,
        /,
        my_user: hikari.OwnUser = dependencies.inject_lc(hikari.OwnUser),
    ) -> bool:
        if ctx.guild_id is None:
            permissions = utilities.DM_PERMISSIONS

        elif ctx.cache and (member := ctx.cache.get_member(ctx.guild_id, my_user)):
            permissions = await utilities.fetch_permissions(ctx.client, member, channel=ctx.channel_id)

        else:
            try:
                member = await ctx.rest.fetch_member(ctx.guild_id, my_user.id)

            except hikari.NotFoundError:
                # If we're not in the Guild then we have to assume the application
                # is still in there and that we likely won't be able to do anything.
                # TODO: re-visit this later.
                return self._handle_result(False)

            permissions = await utilities.fetch_permissions(ctx.client, member, channel=ctx.channel_id)

        return self._handle_result((permissions & self._permissions) == self._permissions)


@typing.overload
def with_dm_check(command: CommandT, /) -> CommandT:
    ...


@typing.overload
def with_dm_check(
    *, error_message: typing.Optional[str] = "Command can only be used in DMs", halt_execution: bool = False
) -> collections.Callable[[CommandT], CommandT]:
    ...


def with_dm_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in DMs",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a DM channel.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in DMs" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, DmCheck(halt_execution=halt_execution, error_message=error_message))


@typing.overload
def with_guild_check(command: CommandT, /) -> CommandT:
    ...


@typing.overload
def with_guild_check(
    *, error_message: typing.Optional[str] = "Command can only be used in guild channels", halt_execution: bool = False
) -> collections.Callable[[CommandT], CommandT]:
    ...


def with_guild_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in guild channels",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a guild channel.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in guild channels" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, GuildCheck(halt_execution=halt_execution, error_message=error_message))


@typing.overload
def with_nsfw_check(command: CommandT, /) -> CommandT:
    ...


@typing.overload
def with_nsfw_check(
    *, error_message: typing.Optional[str] = "Command can only be used in NSFW channels", halt_execution: bool = False
) -> collections.Callable[[CommandT], CommandT]:
    ...


def with_nsfw_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in NSFW channels",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a channel that's marked as nsfw.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in NSFW channels" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, NsfwCheck(halt_execution=halt_execution, error_message=error_message))


@typing.overload
def with_sfw_check(command: CommandT, /) -> CommandT:
    ...


@typing.overload
def with_sfw_check(
    *,
    error_message: typing.Optional[str] = "Command can only be used in SFW channels",
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    ...


def with_sfw_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in SFW channels",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a channel that's marked as sfw.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in SFW channels" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, SfwCheck(halt_execution=halt_execution, error_message=error_message))


@typing.overload
def with_owner_check(command: CommandT, /) -> CommandT:
    ...


@typing.overload
def with_owner_check(
    *,
    error_message: typing.Optional[str] = "Only bot owners can use this command",
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    ...


def with_owner_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Only bot owners can use this command",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run if it's being triggered by one of the bot's owners.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Only bot owners can use this command" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, OwnerCheck(halt_execution=halt_execution, error_message=error_message))


def with_author_permission_check(
    permissions: typing.Union[hikari.Permissions, int],
    *,
    error_message: typing.Optional[str] = "You don't have the permissions required to use this command",
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    """Only let a command run if the author has certain permissions in the current channel.

    Parameters
    ----------
    permissions: typing.Union[hikari.permissions.Permissions, int]
        The permission(s) required for this command to run.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "You don't have the permissions required to use this command" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * This will only pass for commands in DMs if `permissions` is valid for
      a DM context (e.g. can't have any moderation permissions)

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A command decorator callback which adds the check.
    """
    return lambda command: command.add_check(
        AuthorPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message)
    )


def with_own_permission_check(
    permissions: typing.Union[hikari.Permissions, int],
    *,
    error_message: typing.Optional[str] = "Bot doesn't have the permissions required to run this command",
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    """Only let a command run if we have certain permissions in the current channel.

    Parameters
    ----------
    permissions: typing.Union[hikari.permissions.Permissions, int]
        The permission(s) required for this command to run.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Bot doesn't have the permissions required to run this command" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * This will only pass for commands in DMs if `permissions` is valid for
      a DM context (e.g. can't have any moderation permissions)

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A command decorator callback which adds the check.
    """
    return lambda command: command.add_check(
        OwnPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message)
    )


def with_check(check: tanjun_abc.CheckSig, /) -> collections.Callable[[CommandT], CommandT]:
    """Add a generic check to a command.

    Parameters
    ----------
    check : tanjun.abc.CheckSig
        The check to add to this command.

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A command decorator callback which adds the check.
    """
    return lambda command: command.add_check(check)


class _AllChecks(_Check):
    __slots__ = ("_checks",)

    def __init__(self, checks: list[injecting.CallbackDescriptor[bool]]) -> None:
        self._checks = checks

    async def __call__(self, ctx: tanjun_abc.Context, /) -> bool:
        for check in self._checks:
            if not await check.resolve_with_command_context(ctx, ctx):
                return False

        return True


def all_checks(
    check: tanjun_abc.CheckSig,
    /,
    *checks: tanjun_abc.CheckSig,
) -> collections.Callable[[tanjun_abc.Context], collections.Coroutine[typing.Any, typing.Any, bool]]:
    """Combine multiple check callbacks into a check which will only pass if all the callbacks pass.

    This ensures that the callbacks are run in the order they were supplied in
    rather than concurrently.

    Parameters
    ----------
    check : typing_abc.CheckSig
        The first check callback to combine.
    *checks : typing_abc.CheckSig
        Additional check callbacks to combine.

    Returns
    -------
    collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]]
        A check which will pass if all of the provided check callbacks pass.
    """
    checks_ = [injecting.CallbackDescriptor(check)]
    checks_.extend(map(injecting.CallbackDescriptor[bool], checks))
    return _AllChecks(checks_)


def with_all_checks(
    check: tanjun_abc.CheckSig,
    /,
    *checks: tanjun_abc.CheckSig,
) -> collections.Callable[[CommandT], CommandT]:
    """Add a check which will pass if all the provided checks pass through a decorator call.

    This ensures that the callbacks are run in the order they were supplied in
    rather than concurrently.

    Parameters
    ----------
    check : typing_abc.CheckSig
        The first check callback to combine.
    *checks : typing_abc.CheckSig
        Additional check callbacks to combine.

    Returns
    -------
    collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]]
        A check which will pass if all of the provided check callbacks pass.
    """
    return lambda c: c.add_check(all_checks(check, *checks))


class _AnyChecks(_Check):
    __slots__ = ("_checks", "_suppress", "_error_message", "_halt_execution")

    def __init__(
        self,
        checks: list[injecting.CallbackDescriptor[bool]],
        suppress: tuple[type[Exception], ...],
        error_message: typing.Optional[str],
        halt_execution: bool,
    ) -> None:
        self._checks = checks
        self._suppress = suppress
        self._error_message = error_message
        self._halt_execution = halt_execution

    async def __call__(self, ctx: tanjun_abc.Context, /) -> bool:
        for check in self._checks:
            try:
                if await check.resolve_with_command_context(ctx, ctx):
                    return True

            except errors.FailedCheck:
                pass

            except self._suppress:
                pass

        if self._error_message is not None:
            raise errors.CommandError(self._error_message)
        if self._halt_execution:
            raise errors.HaltExecution

        return False


def any_checks(
    check: tanjun_abc.CheckSig,
    /,
    *checks: tanjun_abc.CheckSig,
    suppress: tuple[type[Exception], ...] = (errors.CommandError, errors.HaltExecution),
    error_message: typing.Optional[str],
    halt_execution: bool = False,
) -> collections.Callable[[tanjun_abc.Context], collections.Coroutine[typing.Any, typing.Any, bool]]:
    """Combine multiple checks into a check which'll pass if any of the callbacks pass.

    This ensures that the callbacks are run in the order they were supplied in
    rather than concurrently.

    Parameters
    ----------
    check : typing_abc.CheckSig
        The first check callback to combine.
    *checks : typing_abc.CheckSig
        Additional check callbacks to combine.
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        This takes priority over `halt_execution`.

    Other Parameters
    ----------------
    suppress : tuple[type[Exception], ...]
        Tuple of the exceptions to suppress when a check fails.

        Defaults to (`tanjun.errors.CommandError`, `tanjun.errors.HaltExecution`).
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Returns
    -------
    collections.Callable[[CommandT], CommandT]
        A decorator which adds the generated check to a command.
    """
    checks_ = [injecting.CallbackDescriptor(check)]
    checks_.extend(map(injecting.CallbackDescriptor[bool], checks))
    return _AnyChecks(checks_, suppress, error_message, halt_execution)


def with_any_checks(
    check: tanjun_abc.CheckSig,
    /,
    *checks: tanjun_abc.CheckSig,
    suppress: tuple[type[Exception], ...] = (errors.CommandError, errors.HaltExecution),
    error_message: typing.Optional[str],
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    """Add a check which'll pass if any of the provided checks pass through a decorator call.

    This ensures that the callbacks are run in the order they were supplied in
    rather than concurrently.

    Parameters
    ----------
    check : typing_abc.CheckSig
        The first check callback to combine.
    *checks : typing_abc.CheckSig
        Additional check callbacks to combine.
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        This takes priority over `halt_execution`.

    Other Parameters
    ----------------
    suppress : tuple[type[Exception], ...]
        Tuple of the exceptions to suppress when a check fails.

        Defaults to (`tanjun.errors.CommandError`, `tanjun.errors.HaltExecution`).
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Returns
    -------
    collections.Callable[[CommandT], CommandT]
        A decorator which adds the generated check to a command.
    """
    return lambda c: c.add_check(
        any_checks(check, *checks, suppress=suppress, error_message=error_message, halt_execution=halt_execution)
    )

A collection of common standard checks designed for Tanjun commands.

#   def with_all_checks( check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], /, *checks: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]] ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_all_checks(
    check: tanjun_abc.CheckSig,
    /,
    *checks: tanjun_abc.CheckSig,
) -> collections.Callable[[CommandT], CommandT]:
    """Add a check which will pass if all the provided checks pass through a decorator call.

    This ensures that the callbacks are run in the order they were supplied in
    rather than concurrently.

    Parameters
    ----------
    check : typing_abc.CheckSig
        The first check callback to combine.
    *checks : typing_abc.CheckSig
        Additional check callbacks to combine.

    Returns
    -------
    collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]]
        A check which will pass if all of the provided check callbacks pass.
    """
    return lambda c: c.add_check(all_checks(check, *checks))

Add a check which will pass if all the provided checks pass through a decorator call.

This ensures that the callbacks are run in the order they were supplied in rather than concurrently.

Parameters
  • check (typing_abc.CheckSig): The first check callback to combine.
  • *checks (typing_abc.CheckSig): Additional check callbacks to combine.
Returns
  • collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]]: A check which will pass if all of the provided check callbacks pass.
#   def with_any_checks( check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], /, *checks: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], suppress: tuple[type[Exception], ...] = (<class 'tanjun.errors.CommandError'>, <class 'tanjun.errors.HaltExecution'>), error_message: Optional[str], halt_execution: bool = False ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_any_checks(
    check: tanjun_abc.CheckSig,
    /,
    *checks: tanjun_abc.CheckSig,
    suppress: tuple[type[Exception], ...] = (errors.CommandError, errors.HaltExecution),
    error_message: typing.Optional[str],
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    """Add a check which'll pass if any of the provided checks pass through a decorator call.

    This ensures that the callbacks are run in the order they were supplied in
    rather than concurrently.

    Parameters
    ----------
    check : typing_abc.CheckSig
        The first check callback to combine.
    *checks : typing_abc.CheckSig
        Additional check callbacks to combine.
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        This takes priority over `halt_execution`.

    Other Parameters
    ----------------
    suppress : tuple[type[Exception], ...]
        Tuple of the exceptions to suppress when a check fails.

        Defaults to (`tanjun.errors.CommandError`, `tanjun.errors.HaltExecution`).
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Returns
    -------
    collections.Callable[[CommandT], CommandT]
        A decorator which adds the generated check to a command.
    """
    return lambda c: c.add_check(
        any_checks(check, *checks, suppress=suppress, error_message=error_message, halt_execution=halt_execution)
    )

Add a check which'll pass if any of the provided checks pass through a decorator call.

This ensures that the callbacks are run in the order they were supplied in rather than concurrently.

Parameters
  • check (typing_abc.CheckSig): The first check callback to combine.
  • *checks (typing_abc.CheckSig): Additional check callbacks to combine.
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    This takes priority over halt_execution.

Other Parameters
Returns
  • collections.Callable[[CommandT], CommandT]: A decorator which adds the generated check to a command.
#   def with_check( check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], / ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_check(check: tanjun_abc.CheckSig, /) -> collections.Callable[[CommandT], CommandT]:
    """Add a generic check to a command.

    Parameters
    ----------
    check : tanjun.abc.CheckSig
        The check to add to this command.

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A command decorator callback which adds the check.
    """
    return lambda command: command.add_check(check)

Add a generic check to a command.

Parameters
Returns
  • collections.abc.Callable[[CommandT], CommandT]: A command decorator callback which adds the check.
#   def with_dm_check( command: Optional[~CommandT] = None, /, *, error_message: Optional[str] = 'Command can only be used in DMs', halt_execution: bool = False ) -> Union[~CommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
def with_dm_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in DMs",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a DM channel.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in DMs" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, DmCheck(halt_execution=halt_execution, error_message=error_message))

Only let a command run in a DM channel.

Parameters
  • command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "Command can only be used in DMs" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • For more information on how this is used with other parameters see CallbackReturnT.
Returns
  • CallbackReturnT[CommandT]: The command this check was added to.
#   def with_guild_check( command: Optional[~CommandT] = None, /, *, error_message: Optional[str] = 'Command can only be used in guild channels', halt_execution: bool = False ) -> Union[~CommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
def with_guild_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in guild channels",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a guild channel.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in guild channels" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, GuildCheck(halt_execution=halt_execution, error_message=error_message))

Only let a command run in a guild channel.

Parameters
  • command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "Command can only be used in guild channels" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • For more information on how this is used with other parameters see CallbackReturnT.
Returns
  • CallbackReturnT[CommandT]: The command this check was added to.
#   def with_nsfw_check( command: Optional[~CommandT] = None, /, *, error_message: Optional[str] = 'Command can only be used in NSFW channels', halt_execution: bool = False ) -> Union[~CommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
def with_nsfw_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in NSFW channels",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a channel that's marked as nsfw.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in NSFW channels" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, NsfwCheck(halt_execution=halt_execution, error_message=error_message))

Only let a command run in a channel that's marked as nsfw.

Parameters
  • command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "Command can only be used in NSFW channels" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • For more information on how this is used with other parameters see CallbackReturnT.
Returns
  • CallbackReturnT[CommandT]: The command this check was added to.
#   def with_sfw_check( command: Optional[~CommandT] = None, /, *, error_message: Optional[str] = 'Command can only be used in SFW channels', halt_execution: bool = False ) -> Union[~CommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
def with_sfw_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Command can only be used in SFW channels",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run in a channel that's marked as sfw.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Command can only be used in SFW channels" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, SfwCheck(halt_execution=halt_execution, error_message=error_message))

Only let a command run in a channel that's marked as sfw.

Parameters
  • command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "Command can only be used in SFW channels" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • For more information on how this is used with other parameters see CallbackReturnT.
Returns
  • CallbackReturnT[CommandT]: The command this check was added to.
#   def with_owner_check( command: Optional[~CommandT] = None, /, *, error_message: Optional[str] = 'Only bot owners can use this command', halt_execution: bool = False ) -> Union[~CommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
def with_owner_check(
    command: typing.Optional[CommandT] = None,
    /,
    *,
    error_message: typing.Optional[str] = "Only bot owners can use this command",
    halt_execution: bool = False,
) -> CallbackReturnT[CommandT]:
    """Only let a command run if it's being triggered by one of the bot's owners.

    Parameters
    ----------
    command : typing.Optional[CommandT]
        The command to add this check to.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Only bot owners can use this command" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * For more information on how this is used with other parameters see
      `CallbackReturnT`.

    Returns
    -------
    CallbackReturnT[CommandT]
        The command this check was added to.
    """
    return _optional_kwargs(command, OwnerCheck(halt_execution=halt_execution, error_message=error_message))

Only let a command run if it's being triggered by one of the bot's owners.

Parameters
  • command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "Only bot owners can use this command" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • For more information on how this is used with other parameters see CallbackReturnT.
Returns
  • CallbackReturnT[CommandT]: The command this check was added to.
#   def with_author_permission_check( permissions: Union[hikari.permissions.Permissions, int], *, error_message: Optional[str] = "You don't have the permissions required to use this command", halt_execution: bool = False ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_author_permission_check(
    permissions: typing.Union[hikari.Permissions, int],
    *,
    error_message: typing.Optional[str] = "You don't have the permissions required to use this command",
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    """Only let a command run if the author has certain permissions in the current channel.

    Parameters
    ----------
    permissions: typing.Union[hikari.permissions.Permissions, int]
        The permission(s) required for this command to run.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "You don't have the permissions required to use this command" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * This will only pass for commands in DMs if `permissions` is valid for
      a DM context (e.g. can't have any moderation permissions)

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A command decorator callback which adds the check.
    """
    return lambda command: command.add_check(
        AuthorPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message)
    )

Only let a command run if the author has certain permissions in the current channel.

Parameters
  • permissions (typing.Union[hikari.permissions.Permissions, int]): The permission(s) required for this command to run.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "You don't have the permissions required to use this command" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • This will only pass for commands in DMs if permissions is valid for a DM context (e.g. can't have any moderation permissions)
Returns
  • collections.abc.Callable[[CommandT], CommandT]: A command decorator callback which adds the check.
#   def with_own_permission_check( permissions: Union[hikari.permissions.Permissions, int], *, error_message: Optional[str] = "Bot doesn't have the permissions required to run this command", halt_execution: bool = False ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_own_permission_check(
    permissions: typing.Union[hikari.Permissions, int],
    *,
    error_message: typing.Optional[str] = "Bot doesn't have the permissions required to run this command",
    halt_execution: bool = False,
) -> collections.Callable[[CommandT], CommandT]:
    """Only let a command run if we have certain permissions in the current channel.

    Parameters
    ----------
    permissions: typing.Union[hikari.permissions.Permissions, int]
        The permission(s) required for this command to run.

    Other Parameters
    ----------------
    error_message : typing.Optional[str]
        The error message to send in response as a command error if the check fails.

        Defaults to "Bot doesn't have the permissions required to run this command" and setting this to `None`
        will disable the error message allowing the command search to continue.
    halt_execution : bool
        Whether this check should raise `tanjun.errors.HaltExecution` to
        end the execution search when it fails instead of returning `False`.

        Defaults to `False`.

    Notes
    -----
    * error_message takes priority over halt_execution.
    * This will only pass for commands in DMs if `permissions` is valid for
      a DM context (e.g. can't have any moderation permissions)

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A command decorator callback which adds the check.
    """
    return lambda command: command.add_check(
        OwnPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message)
    )

Only let a command run if we have certain permissions in the current channel.

Parameters
  • permissions (typing.Union[hikari.permissions.Permissions, int]): The permission(s) required for this command to run.
Other Parameters
  • error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.

    Defaults to "Bot doesn't have the permissions required to run this command" and setting this to None will disable the error message allowing the command search to continue.

  • halt_execution (bool): Whether this check should raise tanjun.errors.HaltExecution to end the execution search when it fails instead of returning False.

    Defaults to False.

Notes
  • error_message takes priority over halt_execution.
  • This will only pass for commands in DMs if permissions is valid for a DM context (e.g. can't have any moderation permissions)
Returns
  • collections.abc.Callable[[CommandT], CommandT]: A command decorator callback which adds the check.
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Standard Tanjun client."""
from __future__ import annotations

__all__: list[str] = [
    "as_loader",
    "as_unloader",
    "Client",
    "ClientCallbackNames",
    "MessageAcceptsEnum",
    "PrefixGetterSig",
    "PrefixGetterSigT",
]

import asyncio
import enum
import functools
import importlib
import importlib.abc as importlib_abc
import importlib.util as importlib_util
import inspect
import itertools
import logging
import pathlib
import typing
import warnings
from collections import abc as collections

import hikari
from hikari import traits as hikari_traits

from . import abc as tanjun_abc
from . import checks
from . import context
from . import dependencies
from . import errors
from . import hooks
from . import injecting
from . import utilities

if typing.TYPE_CHECKING:
    import types

    _ClientT = typing.TypeVar("_ClientT", bound="Client")

    class _MessageContextMakerProto(typing.Protocol):
        def __call__(
            self,
            client: tanjun_abc.Client,
            injection_client: injecting.InjectorClient,
            content: str,
            message: hikari.Message,
            *,
            command: typing.Optional[tanjun_abc.MessageCommand[typing.Any]] = None,
            component: typing.Optional[tanjun_abc.Component] = None,
            triggering_name: str = "",
            triggering_prefix: str = "",
        ) -> context.MessageContext:
            raise NotImplementedError

    class _SlashContextMakerProto(typing.Protocol):
        def __call__(
            self,
            client: tanjun_abc.Client,
            injection_client: injecting.InjectorClient,
            interaction: hikari.CommandInteraction,
            *,
            command: typing.Optional[tanjun_abc.BaseSlashCommand] = None,
            component: typing.Optional[tanjun_abc.Component] = None,
            default_to_ephemeral: bool = False,
            on_not_found: typing.Optional[
                collections.Callable[[context.SlashContext], collections.Awaitable[None]]
            ] = None,
        ) -> context.SlashContext:
            raise NotImplementedError


PrefixGetterSig = collections.Callable[..., collections.Awaitable[collections.Iterable[str]]]
"""Type hint of a callable used to get the prefix(es) for a specific guild.

This should be an asynchronous callable which returns an iterable of strings.

.. note::
    While dependency injection is supported for this, the first positional
    argument will always be a `tanjun.abc.MessageContext`.
"""

PrefixGetterSigT = typing.TypeVar("PrefixGetterSigT", bound="PrefixGetterSig")

_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.tanjun.clients")


class _LoaderDescriptor(tanjun_abc.ClientLoader):  # Slots mess with functools.update_wrapper
    def __init__(
        self,
        callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]],
        standard_impl: bool,
    ) -> None:
        self._callback = callback
        self._must_be_std = standard_impl
        functools.update_wrapper(self, callback)

    @property
    def has_load(self) -> bool:
        return True

    @property
    def has_unload(self) -> bool:
        return False

    def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
        self._callback(*args, **kwargs)

    def load(self, client: tanjun_abc.Client, /) -> bool:
        if self._must_be_std:
            if not isinstance(client, Client):
                raise ValueError("This loader requires instances of the standard Client implementation")

            self._callback(client)

        else:
            typing.cast("collections.Callable[[tanjun_abc.Client], None]", self._callback)(client)

        return True

    def unload(self, _: tanjun_abc.Client, /) -> bool:
        return False


class _UnloaderDescriptor(tanjun_abc.ClientLoader):  # Slots mess with functools.update_wrapper
    def __init__(
        self,
        callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]],
        standard_impl: bool,
    ) -> None:
        self._callback = callback
        self._must_be_std = standard_impl
        functools.update_wrapper(self, callback)

    @property
    def has_load(self) -> bool:
        return False

    @property
    def has_unload(self) -> bool:
        return True

    def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
        self._callback(*args, **kwargs)

    def load(self, _: tanjun_abc.Client, /) -> bool:
        return False

    def unload(self, client: tanjun_abc.Client, /) -> bool:
        if self._must_be_std:
            if not isinstance(client, Client):
                raise ValueError("This unloader requires instances of the standard Client implementation")

            self._callback(client)

        else:
            typing.cast("collections.Callable[[tanjun_abc.Client], None]", self._callback)(client)

        return True


@typing.overload
def as_loader(
    callback: collections.Callable[[Client], None], /, *, standard_impl: typing.Literal[True] = True
) -> collections.Callable[[Client], None]:
    ...


@typing.overload
def as_loader(
    callback: collections.Callable[[tanjun_abc.Client], None], /, *, standard_impl: typing.Literal[False]
) -> collections.Callable[[tanjun_abc.Client], None]:
    ...


def as_loader(
    callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]],
    /,
    *,
    standard_impl: bool = True,
) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]:
    """Mark a callback as being used to load Tanjun components from a module.

    .. note::
        This is only necessary if you wish to use `tanjun.Client.load_modules`.

    Parameters
    ----------
    callback : collections.abc.Callable[[tanjun.abc.Client], None]]
        The callback used to load Tanjun components from a module.

        This should take one argument of type `Client` (or `tanjun.abc.Client`
        if `standard_impl` is `False`), return nothing and will be expected
        to initiate and add utilities such as components to the provided client.
    standard_impl : bool
        Whether this loader should only allow instances of `Client` as opposed
        to `tanjun.abc.Client`.

        Defaults to `True`.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.Client], None]]
        The decorated load callback.
    """
    return _LoaderDescriptor(callback, standard_impl)


@typing.overload
def as_unloader(
    callback: collections.Callable[[Client], None], /, *, standard_impl: typing.Literal[True] = True
) -> collections.Callable[[Client], None]:
    ...


@typing.overload
def as_unloader(
    callback: collections.Callable[[tanjun_abc.Client], None], /, *, standard_impl: typing.Literal[False]
) -> collections.Callable[[tanjun_abc.Client], None]:
    ...


def as_unloader(
    callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]],
    /,
    *,
    standard_impl: bool = True,
) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]:
    """Mark a callback as being used to unload a module's utilities from a client.

    .. note::
        This is the inverse of `as_loader` and is only necessary if you wish
        to use the `tanjun.Client.unload_module` or
        `tanjun.Client.reload_module`.

    Parameters
    ----------
    callback : collections.abc.Callable[[tanjun.Client], None]]
        The callback used to unload Tanjun components from a module.

        This should take one argument of type `Client` (or `tanjun.abc.Client`
        if `standard_impl` is `False`), return nothing and will be expected
        to remove utilities such as components from the provided client.
    standard_impl : bool
        Whether this unloader should only allow instances of `Client` as
        opposed to `tanjun.abc.Client`.

        Defaults to `True`.

    Returns
    -------
    collections.abc.Callable[[tanjun.Client], None]]
        The decorated unload callback.
    """
    return _UnloaderDescriptor(callback, standard_impl)


ClientCallbackNames = tanjun_abc.ClientCallbackNames
"""Alias of `tanjun.abc.ClientCallbackNames`."""


class MessageAcceptsEnum(str, enum.Enum):
    """The possible configurations for which events `Client` should execute commands based on."""

    ALL = "ALL"
    """Set the client to execute commands based on both DM and guild message create events."""

    DM_ONLY = "DM_ONLY"
    """Set the client to execute commands based only DM message create events."""

    GUILD_ONLY = "GUILD_ONLY"
    """Set the client to execute commands based only guild message create events."""

    NONE = "NONE"
    """Set the client to not execute commands based on message create events."""

    def get_event_type(self) -> typing.Optional[type[hikari.MessageCreateEvent]]:
        """Get the base event type this mode listens to.

        Returns
        -------
        typing.Optional[type[hikari.message_events.MessageCreateEvent]]
            The type object of the MessageCreateEvent class this mode will
            register a listener for.

            This will be `None` if this mode disables listening to
            message create events.
        """
        return _ACCEPTS_EVENT_TYPE_MAPPING[self]


_ACCEPTS_EVENT_TYPE_MAPPING: dict[MessageAcceptsEnum, typing.Optional[type[hikari.MessageCreateEvent]]] = {
    MessageAcceptsEnum.ALL: hikari.MessageCreateEvent,
    MessageAcceptsEnum.DM_ONLY: hikari.DMMessageCreateEvent,
    MessageAcceptsEnum.GUILD_ONLY: hikari.GuildMessageCreateEvent,
    MessageAcceptsEnum.NONE: None,
}


def _check_human(ctx: tanjun_abc.Context, /) -> bool:
    return ctx.is_human


async def _wrap_client_callback(
    callback: injecting.CallbackDescriptor[None],
    ctx: injecting.AbstractInjectionContext,
    args: tuple[str, ...],
) -> None:
    try:
        await callback.resolve(ctx, *args)

    except Exception as exc:
        _LOGGER.error("Client callback raised exception", exc_info=exc)


async def on_parser_error(ctx: tanjun_abc.Context, error: errors.ParserError) -> None:
    """Handle message parser errors.

    This is the default message parser error hook included by `Client`.
    """
    await ctx.respond(error.message)


def _cmp_command(builder: typing.Optional[hikari.api.CommandBuilder], command: hikari.Command) -> bool:
    if not builder or builder.id is not hikari.UNDEFINED and builder.id != command.id:
        return False

    if builder.name != command.name or builder.description != command.description:
        return False

    default_perm = builder.default_permission if builder.default_permission is not hikari.UNDEFINED else True
    command_options = command.options or ()
    if default_perm is not command.default_permission or len(builder.options) != len(command_options):
        return False

    return all(builder_option == option for builder_option, option in zip(builder.options, command_options))


class _StartDeclarer:
    __slots__ = ("client", "command_ids", "guild_id")

    def __init__(
        self,
        client: Client,
        command_ids: collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]],
        guild_id: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]],
    ) -> None:
        self.client = client
        self.command_ids = command_ids
        self.guild_id = guild_id

    async def __call__(self) -> None:
        try:
            await self.client.declare_global_commands(self.command_ids, guild=self.guild_id, force=False)
        finally:
            self.client.remove_client_callback(ClientCallbackNames.STARTING, self)


class Client(injecting.InjectorClient, tanjun_abc.Client):
    """Tanjun's standard `tanjun.abc.Client` implementation.

    This implementation supports dependency injection for checks, command
    callbacks, prefix getters and event listeners. For more information on how
    this works see `tanjun.injecting`.

    .. note::
        By default this client includes a parser error handling hook which will
        by overwritten if you call `Client.set_hooks`.
    """

    __slots__ = (
        "_accepts",
        "_auto_defer_after",
        "_cache",
        "_cached_application_id",
        "_checks",
        "_client_callbacks",
        "_components",
        "_defaults_to_ephemeral",
        "_make_message_context",
        "_make_slash_context",
        "_events",
        "_grab_mention_prefix",
        "_hooks",
        "_interaction_not_found",
        "_slash_hooks",
        "_is_closing",
        "_listeners",
        "_loop",
        "_message_hooks",
        "_metadata",
        "_modules",
        "_path_modules",
        "_prefix_getter",
        "_prefixes",
        "_rest",
        "_server",
        "_shards",
        "_voice",
    )

    def __init__(
        self,
        rest: hikari.api.RESTClient,
        *,
        cache: typing.Optional[hikari.api.Cache] = None,
        events: typing.Optional[hikari.api.EventManager] = None,
        server: typing.Optional[hikari.api.InteractionServer] = None,
        shards: typing.Optional[hikari_traits.ShardAware] = None,
        voice: typing.Optional[hikari.api.VoiceComponent] = None,
        event_managed: bool = False,
        mention_prefix: bool = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        _stack_level: int = 0,
    ) -> None:
        """Initialise a Tanjun client.

        Notes
        -----
        * For a quicker way to initiate this client around a standard bot aware
        client, see `Client.from_gateway_bot` and `Client.from_rest_bot`.
        * The endpoint used by `declare_global_commands` has a strict ratelimit which,
        as of writing, only allows for 2 requests per minute (with that ratelimit
        either being per-guild if targeting a specific guild otherwise globally).
        * `event_manager` is necessary for message command dispatch and will also
        be necessary for interaction command dispatch if `server` isn't
        provided.
        * `server` is used for interaction command dispatch if interaction
        events aren't being received from the event manager.

        Parameters
        ----------
        rest : hikari.api.rest.RestClient
            The Hikari REST client this will use.

        Other Parameters
        ----------------
        cache : hikari.api.cache.CacheClient
            The Hikari cache client this will use if applicable.
        event_manager : hikari.api.event_manager.EventManagerClient
            The Hikari event manager client this will use if applicable.
        server : hikari.api.interaction_server.InteractionServer
            The Hikari interaction server client this will use if applicable.
        shards : hikari.traits.ShardAware
            The Hikari shard aware client this will use if applicable.
        voice : hikari.api.voice.VoiceComponent
            The Hikari voice component this will use if applicable.
        event_managed : bool
            Whether or not this client is managed by the event manager.

            An event managed client will be automatically started and closed based
            on Hikari's lifetime events.

            Defaults to `False` and can only be passed as `True` if `event_manager`
            is also provided.
        mention_prefix : bool
            Whether or not mention prefixes should be automatically set when this
            client is first started.

            Defaults to `False` and it should be noted that this only applies to
            message commands.
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.

        Raises
        ------
        ValueError
            Raises for the following reasons:
            * If `event_managed` is `True` when `event_manager` is `None`.
            * If `command_ids` is passed when multiple guild ids are provided for `declare_global_commands`.
            * If `command_ids` is passed when `declare_global_commands` is `False`.
        """  # noqa: E501 - line too long
        # InjectorClient.__init__
        super().__init__()
        if _LOGGER.isEnabledFor(logging.INFO):
            _LOGGER.info(
                "%s initialised with the following components: %s",
                "Event-managed client" if event_managed else "Client",
                ", ".join(
                    name
                    for name, value in [
                        ("cache", cache),
                        ("event manager", events),
                        ("interaction server", server),
                        ("rest", rest),
                        ("shard manager", shards),
                    ]
                    if value
                ),
            )

        if not events and not server:
            _LOGGER.warning(
                "Client initiaited without an event manager or interaction server, "
                "automatic command dispatch will be unavailable."
            )

        self._accepts = MessageAcceptsEnum.ALL if events else MessageAcceptsEnum.NONE
        self._auto_defer_after: typing.Optional[float] = 2.0
        self._cache = cache
        self._cached_application_id: typing.Optional[hikari.Snowflake] = None
        self._checks: list[checks.InjectableCheck] = []
        self._client_callbacks: dict[str, list[injecting.CallbackDescriptor[None]]] = {}
        self._components: dict[str, tanjun_abc.Component] = {}
        self._defaults_to_ephemeral: bool = False
        self._make_message_context: _MessageContextMakerProto = context.MessageContext
        self._make_slash_context: _SlashContextMakerProto = context.SlashContext
        self._events = events
        self._grab_mention_prefix = mention_prefix
        self._hooks: typing.Optional[tanjun_abc.AnyHooks] = hooks.AnyHooks().set_on_parser_error(on_parser_error)
        self._interaction_not_found: typing.Optional[str] = "Command not found"
        self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None
        self._is_closing = False
        self._listeners: dict[type[hikari.Event], list[injecting.SelfInjectingCallback[None]]] = {}
        self._loop: typing.Optional[asyncio.AbstractEventLoop] = None
        self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None
        self._metadata: dict[typing.Any, typing.Any] = {}
        self._modules: dict[str, types.ModuleType] = {}
        self._path_modules: dict[pathlib.Path, types.ModuleType] = {}
        self._prefix_getter: typing.Optional[injecting.CallbackDescriptor[collections.Iterable[str]]] = None
        self._prefixes: list[str] = []
        self._rest = rest
        self._server = server
        self._shards = shards
        self._voice = voice

        if event_managed:
            if not events:
                raise ValueError("Client cannot be event managed without an event manager")

            events.subscribe(hikari.StartingEvent, self._on_starting_event)
            events.subscribe(hikari.StoppingEvent, self._on_stopping_event)

        if set_global_commands:
            warnings.warn(
                "The `set_global_commands` argument is deprecated since v2.1.1a1. "
                "Use `declare_global_commands` instead.",
                DeprecationWarning,
                stacklevel=2 + _stack_level,
            )

        declare_global_commands = declare_global_commands or set_global_commands
        command_ids = command_ids or {}
        if isinstance(declare_global_commands, collections.Sequence):
            if command_ids and len(declare_global_commands) > 1:
                raise ValueError(
                    "Cannot provide specific command_ids while automatically "
                    "declaring commands marked as 'global' in multiple-guilds on startup"
                )

            for guild in declare_global_commands:
                _LOGGER.info("Registering startup command declarer for %s guild", guild)
                self.add_client_callback(ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, guild))

        elif isinstance(declare_global_commands, bool):
            if declare_global_commands:
                _LOGGER.info("Registering startup command declarer for global commands")
                if not command_ids:
                    _LOGGER.warning(
                        "No command IDs passed for startup command declarer, this could lead to previously set "
                        "command permissions being lost when commands are renamed."
                    )

                self.add_client_callback(
                    ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, hikari.UNDEFINED)
                )

            elif command_ids:
                raise ValueError("Cannot pass command IDs when not declaring global commands")

        else:
            self.add_client_callback(
                ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, declare_global_commands)
            )

        (
            self.set_type_dependency(tanjun_abc.Client, self)
            .set_type_dependency(Client, self)
            .set_type_dependency(type(self), self)
            .set_type_dependency(hikari.api.RESTClient, rest)
            .set_type_dependency(type(rest), rest)
        )
        if cache:
            self.set_type_dependency(hikari.api.Cache, cache).set_type_dependency(type(cache), cache)

        if events:
            self.set_type_dependency(hikari.api.EventManager, events).set_type_dependency(type(events), events)

        if server:
            self.set_type_dependency(hikari.api.InteractionServer, server).set_type_dependency(type(server), server)

        if shards:
            self.set_type_dependency(hikari_traits.ShardAware, shards).set_type_dependency(type(shards), shards)

        if voice:
            self.set_type_dependency(hikari.api.VoiceComponent, voice).set_type_dependency(type(voice), voice)

        dependencies.set_standard_dependencies(self)

    @classmethod
    def from_gateway_bot(
        cls,
        bot: hikari_traits.GatewayBotAware,
        /,
        *,
        event_managed: bool = True,
        mention_prefix: bool = False,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
    ) -> Client:
        """Build a `Client` from a `hikari.traits.GatewayBotAware` instance.

        Notes
        -----
        * This implicitly defaults the client to human only mode.
        * This sets type dependency injectors for the hikari traits present in
          `bot` (including `hikari.traits.GatewayBotAware`).
        * The endpoint used by `declare_global_commands` has a strict ratelimit
          which, as of writing, only allows for 2 requests per minute (with that
          ratelimit either being per-guild if targeting a specific guild
          otherwise globally).

        Parameters
        ----------
        bot : hikari.traits.GatewayBotAware
            The bot client to build from.

            This will be used to infer the relevant Hikari clients to use.

        Other Parameters
        ----------------
        event_managed : bool
            Whether or not this client is managed by the event manager.

            An event managed client will be automatically started and closed
            based on Hikari's lifetime events.

            Defaults to `True`.
        mention_prefix : bool
            Whether or not mention prefixes should be automatically set when this
            client is first started.

            Defaults to `False` and it should be noted that this only applies to
            message commands.
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.
        """  # noqa: E501 - line too long
        return (
            cls(
                rest=bot.rest,
                cache=bot.cache,
                events=bot.event_manager,
                shards=bot,
                voice=bot.voice,
                event_managed=event_managed,
                mention_prefix=mention_prefix,
                declare_global_commands=declare_global_commands,
                set_global_commands=set_global_commands,
                command_ids=command_ids,
                _stack_level=1,
            )
            .set_human_only()
            .set_hikari_trait_injectors(bot)
        )

    @classmethod
    def from_rest_bot(
        cls,
        bot: hikari_traits.RESTBotAware,
        /,
        *,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
    ) -> Client:
        """Build a `Client` from a `hikari.traits.RESTBotAware` instance.

        Notes
        -----
        * This sets type dependency injectors for the hikari traits present in
          `bot` (including `hikari.traits.RESTBotAware`).
        * The endpoint used by `declare_global_commands` has a strict ratelimit
          which, as of writing, only allows for 2 requests per minute (with that
          ratelimit either being per-guild if targeting a specific guild
          otherwise globally).

        Parameters
        ----------
        bot : hikari.traits.RESTBotAware
            The bot client to build from.

        Other Parameters
        ----------------
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.
        """  # noqa: E501 - line too long
        return cls(
            rest=bot.rest,
            server=bot.interaction_server,
            declare_global_commands=declare_global_commands,
            set_global_commands=set_global_commands,
            command_ids=command_ids,
            _stack_level=1,
        ).set_hikari_trait_injectors(bot)

    async def __aenter__(self) -> Client:
        await self.open()
        return self

    async def __aexit__(
        self,
        exc_type: typing.Optional[type[Exception]],
        exc: typing.Optional[Exception],
        exc_traceback: typing.Optional[types.TracebackType],
    ) -> None:
        await self.close()

    def __repr__(self) -> str:
        return f"CommandClient <{type(self).__name__!r}, {len(self._components)} components, {self._prefixes}>"

    @property
    def defaults_to_ephemeral(self) -> bool:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._defaults_to_ephemeral

    @property
    def message_accepts(self) -> MessageAcceptsEnum:
        """Type of message create events this command client accepts for execution."""
        return self._accepts

    @property
    def is_human_only(self) -> bool:
        """Whether this client is only executing for non-bot/webhook users messages."""
        return typing.cast("checks.InjectableCheck", _check_human) in self._checks

    @property
    def cache(self) -> typing.Optional[hikari.api.Cache]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._cache

    @property
    def checks(self) -> collections.Collection[tanjun_abc.CheckSig]:
        """Collection of the level `tanjun.abc.Context` checks registered to this client.

        .. note::
            These may be taking advantage of the standard dependency injection.
        """
        return tuple(check.callback for check in self._checks)

    @property
    def components(self) -> collections.Collection[tanjun_abc.Component]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._components.copy().values()

    @property
    def events(self) -> typing.Optional[hikari.api.EventManager]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._events

    @property
    def listeners(
        self,
    ) -> collections.Mapping[type[hikari.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]:
        return utilities.CastedView(
            self._listeners,
            lambda x: [typing.cast(tanjun_abc.ListenerCallbackSig, callback.callback) for callback in x],
        )

    @property
    def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]:
        """Top level `tanjun.abc.AnyHooks` set for this client.

        These are called during both message and interaction command execution.

        Returns
        -------
        typing.Optional[tanjun.abc.AnyHooks]
            The top level `tanjun.abc.Context` based hooks set for this
            client if applicable, else `None`.
        """
        return self._hooks

    @property
    def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]:
        """Top level `tanjun.abc.SlashHooks` set for this client.

        These are only called during interaction command execution.
        """
        return self._slash_hooks

    @property
    def is_alive(self) -> bool:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._loop is not None

    @property
    def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._loop

    @property
    def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]:
        """Top level `tanjun.abc.MessageHooks` set for this client.

        These are only called during both message command execution.
        """
        return self._message_hooks

    @property
    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._metadata

    @property
    def prefix_getter(self) -> typing.Optional[PrefixGetterSig]:
        """Prefix getter method set for this client.

        For more information on this callback's signature see `PrefixGetter`.
        """
        return typing.cast(PrefixGetterSig, self._prefix_getter.callback) if self._prefix_getter else None

    @property
    def prefixes(self) -> collections.Collection[str]:
        """Collection of the standard prefixes set for this client."""
        return self._prefixes.copy()

    @property
    def rest(self) -> hikari.api.RESTClient:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._rest

    @property
    def server(self) -> typing.Optional[hikari.api.InteractionServer]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._server

    @property
    def shards(self) -> typing.Optional[hikari_traits.ShardAware]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._shards

    @property
    def voice(self) -> typing.Optional[hikari.api.VoiceComponent]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._voice

    async def _on_starting_event(self, _: hikari.StartingEvent, /) -> None:
        await self.open()

    async def _on_stopping_event(self, _: hikari.StoppingEvent, /) -> None:
        await self.close()

    async def clear_application_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        if application is None:
            application = self._cached_application_id or await self.fetch_rest_application_id()

        await self._rest.set_application_commands(application, (), guild=guild)

    async def set_global_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        """Alias of `Client.declare_global_commands`.

        .. deprecated:: v2.1.1a1
            Use `Client.declare_global_commands` instead.
        """
        warnings.warn(
            "The `Client.set_global_commands` method has been deprecated since v2.1.1a1. "
            "Use `Client.declare_global_commands` instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return await self.declare_global_commands(application=application, guild=guild, force=force)

    async def declare_global_commands(
        self,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        # <<inherited docstring from tanjun.abc.Client>>.
        commands = (
            command
            for command in itertools.chain.from_iterable(
                component.slash_commands for component in self._components.values()
            )
            if command.is_global
        )
        return await self.declare_application_commands(
            commands, command_ids, application=application, guild=guild, force=force
        )

    async def declare_application_command(
        self,
        command: tanjun_abc.BaseSlashCommand,
        /,
        command_id: typing.Optional[hikari.Snowflakeish] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> hikari.Command:
        # <<inherited docstring from tanjun.abc.Client>>.
        builder = command.build()
        if command_id:
            response = await self._rest.edit_application_command(
                application or self._cached_application_id or await self.fetch_rest_application_id(),
                command_id,
                guild=guild,
                name=builder.name,
                description=builder.description,
                options=builder.options,
            )

        else:
            response = await self._rest.create_application_command(
                application or self._cached_application_id or await self.fetch_rest_application_id(),
                guild=guild,
                name=builder.name,
                description=builder.description,
                options=builder.options,
            )

        if not guild:
            command.set_tracked_command(response)  # TODO: is this fine?

        return response

    async def declare_application_commands(
        self,
        commands: collections.Iterable[tanjun_abc.BaseSlashCommand],
        /,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        # <<inherited docstring from tanjun.abc.Client>>.
        command_ids = command_ids or {}
        names_to_commands: dict[str, tanjun_abc.BaseSlashCommand] = {}
        conflicts: set[str] = set()
        builders: dict[str, hikari.api.CommandBuilder] = {}

        for command in commands:
            names_to_commands[command.name] = command
            if command.name in builders:
                conflicts.add(command.name)

            builder = command.build()
            if command_id := command_ids.get(command.name):
                builder.set_id(hikari.Snowflake(command_id))

            builders[command.name] = builder

        if conflicts:
            raise ValueError(
                "Couldn't declare commands due to conflicts. The following command names have more than one command "
                "registered for them " + ", ".join(conflicts)
            )

        if len(builders) > 100:
            raise ValueError("You can only declare up to 100 top level commands in a guild or globally")

        if not application:
            application = self._cached_application_id or await self.fetch_rest_application_id()

        target_type = "global" if guild is hikari.UNDEFINED else f"guild {int(guild)}"

        if not force:
            registered_commands = await self._rest.fetch_application_commands(application, guild=guild)
            if len(registered_commands) == len(builders) and all(
                _cmp_command(builders.get(command.name), command) for command in registered_commands
            ):
                _LOGGER.info("Skipping bulk declare for %s slash commands since they're already declared", target_type)
                return registered_commands

        _LOGGER.info("Bulk declaring %s %s slash commands", len(builders), target_type)
        responses = await self._rest.set_application_commands(application, list(builders.values()), guild=guild)

        for response in responses:
            if not guild:
                names_to_commands[response.name].set_tracked_command(response)  # TODO: is this fine?

            if (expected_id := command_ids.get(response.name)) and hikari.Snowflake(expected_id) != response.id:
                _LOGGER.warning(
                    "ID mismatch found for %s command %r, expected %s but got %s. "
                    "This suggests that any previous permissions set for this command will have been lost.",
                    target_type,
                    response.name,
                    expected_id,
                    response.id,
                )

        _LOGGER.info("Successfully declared %s (top-level) %s commands", len(responses), target_type)
        if _LOGGER.isEnabledFor(logging.DEBUG):
            _LOGGER.debug(
                "Declared %s command ids; %s",
                target_type,
                ", ".join(f"{response.name}: {response.id}" for response in responses),
            )

        return responses

    def set_auto_defer_after(self: _ClientT, time: typing.Optional[float], /) -> _ClientT:
        """Set when this client should automatically defer execution of commands.

        .. warning::
            If `time` is set to `None` then automatic deferrals will be disabled.
            This may lead to unexpected behaviour.

        Parameters
        ----------
        time : typing.Optional[float]
            The time in seconds to defer interaction command responses after.
        """
        self._auto_defer_after = float(time) if time is not None else None
        return self

    def set_ephemeral_default(self: _ClientT, state: bool, /) -> _ClientT:
        """Set whether slash contexts spawned by this client should default to ephemeral responses.

        Parameters
        ----------
        bool
            Whether slash command contexts executed in this component should
            should default to ephemeral.

            This will be overridden by any response calls which specify flags
            and defaults to `False`.

        Returns
        -------
        SelfT
            This component to enable method chaining.
        """
        self._defaults_to_ephemeral = state
        return self

    def set_hikari_trait_injectors(self: _ClientT, bot: hikari_traits.RESTAware, /) -> _ClientT:
        """Set type based dependency injection based on the hikari traits found in `bot`.

        This is a short hand for calling `Client.add_type_dependency` for all
        the hikari trait types `bot` is valid for with bot.

        Parameters
        ----------
        bot : hikari_traits.RESTAware
            The hikari client to set dependency injectors for.
        """
        for _, member in inspect.getmembers(hikari_traits):
            if inspect.isclass(member) and isinstance(bot, member):
                self.set_type_dependency(member, bot)

        return self

    def set_interaction_not_found(self: _ClientT, message: typing.Optional[str], /) -> _ClientT:
        """Set the response message for when an interaction command is not found.

        .. warning::
            Setting this to `None` may lead to unexpected behaviour (especially
            when the client is still set to auto-defer interactions) and should
            only be done if you know what you're doing.

        Parameters
        ----------
        message : typing.Optional[str]
            The message to respond with when an interaction command isn't found.
        """
        self._interaction_not_found = message
        return self

    def set_message_accepts(self: _ClientT, accepts: MessageAcceptsEnum, /) -> _ClientT:
        """Set the kind of messages commands should be executed based on.

        Parameters
        ----------
        accepts : MessageAcceptsEnum
            The type of messages commands should be executed based on.
        """
        if accepts.get_event_type() and not self._events:
            raise ValueError("Cannot set accepts level on a client with no event manager")

        self._accepts = accepts
        return self

    def set_message_ctx_maker(self: _ClientT, maker: _MessageContextMakerProto = context.MessageContext, /) -> _ClientT:
        """Set the message context maker to use when creating context for a message.

        .. warning::
            The caller must return an instance of `tanjun.context.MessageContext`
            rather than just any implementation of the MessageContext abc due to
            this client relying on implementation detail of
            `tanjun.context.MessageContext`.

        Parameters
        ----------
        maker : _MessageContextMakerProto
            The message context maker to use.

            This is a callback which should match the signature of
            `tanjun.context.MessageContext.__init__` and return an instance
            of `tanjun.context.MessageContext`.

            This defaults to `tanjun.context.MessageContext`.
        """
        self._make_message_context = maker
        return self

    def set_metadata(self: _ClientT, key: typing.Any, value: typing.Any, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        self._metadata[key] = value
        return self

    def set_slash_ctx_maker(self: _ClientT, maker: _SlashContextMakerProto = context.SlashContext, /) -> _ClientT:
        """Set the slash context maker to use when creating context for a slash command.

        .. warning::
            The caller must return an instance of `tanjun.context.SlashContext`
            rather than just any implementation of the SlashContext abc due to
            this client relying on implementation detail of
            `tanjun.context.SlashContext`.

        Parameters
        ----------
        maker : _SlashContextMakerProto
            The slash context maker to use.

            This is a callback which should match the signature of
            `tanjun.context.SlashContext.__init__` and return an instance
            of `tanjun.context.SlashContext`.

            This defaults to `tanjun.context.SlashContext`.
        """
        self._make_slash_context = maker
        return self

    def set_human_only(self: _ClientT, value: bool = True) -> _ClientT:
        """Set whether or not message commands execution should be limited to "human" users.

        .. note::
            This doesn't apply to interaction commands as these can only be
            triggered by a "human" (normal user account).

        Parameters
        ----------
        value : bool
            Whether or not message commands execution should be limited to "human" users.

            Passing `True` here will prevent message commands from being executed
            based on webhook and bot messages.
        """
        if value:
            self.add_check(_check_human)

        else:
            try:
                self.remove_check(_check_human)
            except ValueError:
                pass

        return self

    def add_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT:
        """Add a generic check to this client.

        This will be applied to both message and slash command execution.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to add. This may be either synchronous or asynchronous
            and must take one positional argument of type `tanjun.abc.Context`
            with dependency injection being supported for its keyword arguments.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        if check not in self._checks:
            self._checks.append(checks.InjectableCheck(check))

        return self

    def remove_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT:
        """Remove a check from the client.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to remove.

        Raises
        ------
        ValueError
            If the check was not previously added.
        """
        self._checks.remove(typing.cast("checks.InjectableCheck", check))
        return self

    def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT:
        """Add a check to this client through a decorator call.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to add. This may be either synchronous or asynchronous
            and must take one positional argument of type `tanjun.abc.Context`
            with dependency injection being supported for its keyword arguments.

        Returns
        -------
        tanjun_abc.CheckSig
            The added check.
        """
        self.add_check(check)
        return check

    async def check(self, ctx: tanjun_abc.Context, /) -> bool:
        return await utilities.gather_checks(ctx, self._checks)

    def add_component(self: _ClientT, component: tanjun_abc.Component, /, *, add_injector: bool = False) -> _ClientT:
        """Add a component to this client.

        Parameters
        ----------
        component: Component
            The component to move to this client.

        Returns
        -------
        Self
            The client instance to allow chained calls.

        Raises
        ------
        ValueError
            If the component's name is already registered.
        """
        if component.name in self._components:
            raise ValueError(f"A component named {component.name!r} is already registered.")

        component.bind_client(self)
        self._components[component.name] = component

        if add_injector:
            self.set_type_dependency(type(component), lambda: component)

        if self._loop:
            self._loop.create_task(component.open())
            self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.COMPONENT_ADDED, component))

        return self

    def get_component_by_name(self, name: str, /) -> typing.Optional[tanjun_abc.Component]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._components.get(name)

    def remove_component(self: _ClientT, component: tanjun_abc.Component, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        stored_component = self._components.get(component.name)
        if not stored_component or stored_component != component:
            raise ValueError(f"The component {component!r} is not registered.")

        del self._components[component.name]

        if self._loop:
            self._loop.create_task(component.close(unbind=True))
            self._loop.create_task(
                self.dispatch_client_callback(ClientCallbackNames.COMPONENT_REMOVED, stored_component)
            )

        else:
            stored_component.unbind_client(self)

        return self

    def remove_component_by_name(self: _ClientT, name: str, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self.remove_component(self._components[name])

    def add_client_callback(
        self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        descriptor = injecting.CallbackDescriptor(callback)
        name = name.casefold()
        try:
            if descriptor in self._client_callbacks[name]:
                return self

            self._client_callbacks[name].append(descriptor)
        except KeyError:
            self._client_callbacks[name] = [descriptor]

        return self

    async def dispatch_client_callback(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /, *args: typing.Any
    ) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        if callbacks := self._client_callbacks.get(name):
            calls = (
                _wrap_client_callback(callback, injecting.BasicInjectionContext(self), args) for callback in callbacks
            )
            await asyncio.gather(*calls)

    def get_client_callbacks(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Collection[tanjun_abc.MetaEventSig]:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        if result := self._client_callbacks.get(name):
            return tuple(callback.callback for callback in result)

        return ()

    def remove_client_callback(
        self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        self._client_callbacks[name].remove(typing.cast("injecting.CallbackDescriptor[None]", callback))
        if not self._client_callbacks[name]:
            del self._client_callbacks[name]

        return self

    def with_client_callback(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]:
        # <<inherited docstring from tanjun.abc.Client>>.
        def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT:
            self.add_client_callback(name, callback)
            return callback

        return decorator

    def add_listener(
        self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        injected: injecting.SelfInjectingCallback[None] = injecting.SelfInjectingCallback(self, callback)
        try:
            if callback in self._listeners[event_type]:
                return self

            self._listeners[event_type].append(injected)

        except KeyError:
            self._listeners[event_type] = [injected]

        if self._loop and self._events:
            self._events.subscribe(event_type, injected.__call__)

        return self

    def remove_listener(
        self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        index = self._listeners[event_type].index(typing.cast("injecting.SelfInjectingCallback[None]", callback))
        registered_callback = self._listeners[event_type].pop(index)

        if not self._listeners[event_type]:
            del self._listeners[event_type]

        if self._loop and self._events:
            self._events.unsubscribe(event_type, registered_callback.__call__)

        return self

    def with_listener(
        self, event_type: type[hikari.Event], /
    ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]:
        # <<inherited docstring from tanjun.abc.Client>>.
        def decorator(callback: tanjun_abc.ListenerCallbackSigT, /) -> tanjun_abc.ListenerCallbackSigT:
            self.add_listener(event_type, callback)
            return callback

        return decorator

    def add_prefix(self: _ClientT, prefixes: typing.Union[collections.Iterable[str], str], /) -> _ClientT:
        """Add a prefix used to filter message command calls.

        This will be matched against the first character(s) in a message's
        content to determine whether the message command search stage of
        execution should be initiated.

        Parameters
        ----------
        prefixes : typing.Union[collections.abc.Iterable[str], str]
            Either a single string or an iterable of strings to be used as
            prefixes.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        if isinstance(prefixes, str):
            if prefixes not in self._prefixes:
                self._prefixes.append(prefixes)

        else:
            self._prefixes.extend(prefix for prefix in prefixes if prefix not in self._prefixes)

        return self

    def remove_prefix(self: _ClientT, prefix: str, /) -> _ClientT:
        """Remove a message content prefix from the client.

        Parameters
        ----------
        prefix : str
            The prefix to remove.

        Raises
        ------
        ValueError
            If the prefix is not registered with the client.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._prefixes.remove(prefix)
        return self

    def set_prefix_getter(self: _ClientT, getter: typing.Optional[PrefixGetterSig], /) -> _ClientT:
        """Set the callback used to retrieve message prefixes set for the relevant guild.

        Parameters
        ----------
        getter : typing.Optional[PrefixGetterSig]
            The callback which'll be used to retrieve prefixes for the guild a
            message context is from. If `None` is passed here then the callback
            will be unset.

            This should be an async callback which one argument of type
            `tanjun.abc.MessageContext` and returns an iterable of string prefixes.
            Dependency injection is supported for this callback's keyword arguments.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._prefix_getter = injecting.CallbackDescriptor(getter) if getter else None
        return self

    def with_prefix_getter(self, getter: PrefixGetterSigT, /) -> PrefixGetterSigT:
        """Set the prefix getter callback for this client through decorator call.

        Examples
        --------
        ```py
        client = tanjun.Client.from_rest_bot(bot)

        @client.with_prefix_getter
        async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]:
            raise NotImplementedError
        ```

        Parameters
        ----------
        getter : PrefixGetterSig
            The callback which'll be  to retrieve prefixes for the guild a
            message event is from.

            This should be an async callback which one argument of type
            `tanjun.abc.MessageContext` and returns an iterable of string prefixes.
            Dependency injection is supported for this callback's keyword arguments.

        Returns
        -------
        PrefixGetterSigT
            The registered callback.
        """
        self.set_prefix_getter(getter)
        return getter

    def iter_commands(self) -> collections.Iterator[tanjun_abc.ExecutableCommand[tanjun_abc.Context]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        slash_commands = self.iter_slash_commands(global_only=False)
        yield from self.iter_message_commands()
        yield from slash_commands

    def iter_message_commands(self) -> collections.Iterator[tanjun_abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(component.message_commands for component in self.components)

    def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Client>>.
        if global_only:
            return filter(lambda c: c.is_global, self.iter_slash_commands(global_only=False))

        return itertools.chain.from_iterable(component.slash_commands for component in self.components)

    def check_message_name(
        self, name: str, /
    ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(
            component.check_message_name(name) for component in self._components.values()
        )

    def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(
            component.check_slash_name(name) for component in self._components.values()
        )

    async def _check_prefix(self, ctx: tanjun_abc.MessageContext, /) -> typing.Optional[str]:
        if self._prefix_getter:
            for prefix in await self._prefix_getter.resolve_with_command_context(ctx, ctx):
                if ctx.content.startswith(prefix):
                    return prefix

        for prefix in self._prefixes:
            if ctx.content.startswith(prefix):
                return prefix

        return None

    def _try_unsubscribe(
        self,
        event_manager: hikari.api.EventManager,
        event_type: type[hikari.Event],
        callback: tanjun_abc.ListenerCallbackSig,
    ) -> None:
        try:
            event_manager.unsubscribe(event_type, callback)
        except (ValueError, LookupError):
            # TODO: add logging here
            pass

    async def close(self, *, deregister_listeners: bool = True) -> None:
        """Close the client.

        Raises
        ------
        RuntimeError
            If the client isn't running.
        """
        if not self._loop:
            raise RuntimeError("Client isn't active")

        if self._is_closing:
            event = asyncio.Event()
            self.add_client_callback(ClientCallbackNames.CLOSED, event.set)
            try:
                await event.wait()
            finally:
                self.remove_client_callback(ClientCallbackNames.CLOSED, event.set)
            return

        self._is_closing = True
        await self.dispatch_client_callback(ClientCallbackNames.CLOSING)
        if deregister_listeners and self._events:
            if event_type := self._accepts.get_event_type():
                self._try_unsubscribe(self._events, event_type, self.on_message_create_event)

            self._try_unsubscribe(self._events, hikari.InteractionCreateEvent, self.on_interaction_create_event)

            for event_type_, listeners in self._listeners.items():
                for listener in listeners:
                    self._try_unsubscribe(self._events, event_type_, listener.__call__)

        if deregister_listeners and self._server:
            self._server.set_listener(hikari.CommandInteraction, None)

        await asyncio.gather(*(component.close() for component in self._components.copy().values()))

        self._loop = None
        await self.dispatch_client_callback(ClientCallbackNames.CLOSED)
        self._is_closing = False

    async def open(self, *, register_listeners: bool = True) -> None:
        """Start the client.

        If `mention_prefix` was passed to `Client.__init__` or
        `Client.from_gateway_bot` then this function may make a fetch request
        to Discord if it cannot get the current user from the cache.

        Raises
        ------
        RuntimeError
            If the client is already active.
        """
        if self._loop:
            raise RuntimeError("Client is already alive")

        self._loop = asyncio.get_running_loop()
        self._is_closing = False
        await self.dispatch_client_callback(ClientCallbackNames.STARTING)

        if self._grab_mention_prefix:
            user: typing.Optional[hikari.OwnUser] = None
            if self._cache:
                user = self._cache.get_me()

            if not user and (user_cache := self.get_type_dependency(dependencies.SingleStoreCache[hikari.OwnUser])):
                user = await user_cache.get(default=None)

            if not user:
                user = await self._rest.fetch_my_user()

            for prefix in f"<@{user.id}>", f"<@!{user.id}>":
                if prefix not in self._prefixes:
                    self._prefixes.append(prefix)

            self._grab_mention_prefix = False

        await asyncio.gather(*(component.open() for component in self._components.copy().values()))

        if register_listeners and self._events:
            if event_type := self._accepts.get_event_type():
                self._events.subscribe(event_type, self.on_message_create_event)

            self._events.subscribe(hikari.InteractionCreateEvent, self.on_interaction_create_event)

            for event_type_, listeners in self._listeners.items():
                for listener in listeners:
                    self._events.subscribe(event_type_, listener.__call__)

        if register_listeners and self._server:
            self._server.set_listener(hikari.CommandInteraction, self.on_interaction_create_request)

        self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.STARTED))

    async def fetch_rest_application_id(self) -> hikari.Snowflake:
        """Fetch the ID of the application this client is linked to.

        Returns
        -------
        hikari.Snowflake
            The application ID of the application this client is linked to.
        """
        if self._cached_application_id:
            return self._cached_application_id

        application_cache = self.get_type_dependency(
            dependencies.SingleStoreCache[hikari.Application]
        ) or self.get_type_dependency(dependencies.SingleStoreCache[hikari.AuthorizationApplication])
        if application_cache and (application := await application_cache.get(default=None)):
            self._cached_application_id = application.id
            return application.id

        if self._rest.token_type == hikari.TokenType.BOT:
            self._cached_application_id = hikari.Snowflake(await self._rest.fetch_application())

        else:
            self._cached_application_id = hikari.Snowflake((await self._rest.fetch_authorization()).application)

        return self._cached_application_id

    def set_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ClientT:
        """Set the general command execution hooks for this client.

        The callbacks within this hook will be added to every slash and message
        command execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.AnyHooks]
            The general command execution hooks to set for this client.

            Passing `None` will remove all hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._hooks = hooks
        return self

    def set_slash_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.SlashHooks], /) -> _ClientT:
        """Set the slash command execution hooks for this client.

        The callbacks within this hook will be added to every slash command
        execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.SlashHooks]
            The slash context specific command execution hooks to set for this
            client.

            Passing `None` will remove the hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._slash_hooks = hooks
        return self

    def set_message_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.MessageHooks], /) -> _ClientT:
        """Set the message command execution hooks for this client.

        The callbacks within this hook will be added to every message command
        execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.MessageHooks]
            The message context specific command execution hooks to set for this
            client.

            Passing `None` will remove all hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._message_hooks = hooks
        return self

    def _call_loaders(
        self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], /
    ) -> None:
        found = False
        for loader in loaders:
            if loader.load(self):
                found = True

        if not found:
            raise errors.ModuleMissingLoaders(f"Didn't find any loaders in {module_path}", module_path)

    def _call_unloaders(
        self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], /
    ) -> None:
        found = False
        for loader in loaders:
            if loader.unload(self):
                found = True

        if not found:
            raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in {module_path}", module_path)

    def _load_module(
        self, module_path: typing.Union[str, pathlib.Path]
    ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]:
        if isinstance(module_path, str):
            if module_path in self._modules:
                raise errors.ModuleStateConflict(f"module {module_path} already loaded", module_path)

            _LOGGER.info("Loading from %s", module_path)
            module = yield lambda: importlib.import_module(module_path)

            with _WrapLoadError(errors.FailedModuleLoad):
                self._call_loaders(module_path, _get_loaders(module, module_path))

            self._modules[module_path] = module

        else:
            module_path_abs = module_path.absolute()
            if module_path_abs in self._path_modules:
                raise errors.ModuleStateConflict(f"Module at {module_path} already loaded", module_path)

            _LOGGER.info("Loading from %s", module_path)
            module = yield lambda: _get_path_module(module_path)

            with _WrapLoadError(errors.FailedModuleLoad):
                self._call_loaders(module_path, _get_loaders(module, module_path))

            self._path_modules[module_path_abs] = module

    def load_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = module_path.absolute()

            generator = self._load_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = load_module()

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

        return self

    async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        loop = asyncio.get_running_loop()
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = await loop.run_in_executor(None, module_path.absolute)

            generator = self._load_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = await loop.run_in_executor(None, load_module)

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

    def unload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.ab.Client>>.
        for module_path in modules:
            if isinstance(module_path, str):
                modules_dict: dict[typing.Any, types.ModuleType] = self._modules

            else:
                modules_dict = self._path_modules
                module_path = module_path.absolute()

            module = modules_dict.get(module_path)
            if not module:
                raise errors.ModuleStateConflict(f"Module {module_path!s} not loaded", module_path)

            _LOGGER.info("Unloading from %s", module_path)
            with _WrapLoadError(errors.FailedModuleUnload):
                self._call_unloaders(module_path, _get_loaders(module, module_path))

            del modules_dict[module_path]

        return self

    def _reload_module(
        self, module_path: typing.Union[str, pathlib.Path]
    ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]:
        if isinstance(module_path, str):
            old_module = self._modules.get(module_path)

            def load_module() -> types.ModuleType:
                assert old_module
                return importlib.reload(old_module)

            modules_dict: dict[typing.Any, types.ModuleType] = self._modules

        else:
            old_module = self._path_modules.get(module_path)

            def load_module() -> types.ModuleType:
                assert isinstance(module_path, pathlib.Path)
                return _get_path_module(module_path)

            modules_dict = self._path_modules

        if not old_module:
            raise errors.ModuleStateConflict(f"Module {module_path} not loaded", module_path)

        _LOGGER.info("Reloading %s", module_path)

        old_loaders = _get_loaders(old_module, module_path)
        # We assert that the old module has unloaders early to avoid unnecessarily
        # importing the new module.
        if not any(loader.has_unload for loader in old_loaders):
            raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in old {module_path}", module_path)

        module = yield load_module

        loaders = _get_loaders(module, module_path)

        # We assert that the new module has loaders early to avoid unnecessarily
        # unloading then rolling back when we know it's going to fail to load.
        if not any(loader.has_load for loader in loaders):
            raise errors.ModuleMissingLoaders(f"Didn't find any loaders in new {module_path}", module_path)

        with _WrapLoadError(errors.FailedModuleUnload):
            # This will never raise MissingLoaders as we assert this earlier
            self._call_unloaders(module_path, old_loaders)

        try:
            # This will never raise MissingLoaders as we assert this earlier
            self._call_loaders(module_path, loaders)
        except Exception as exc:
            self._call_loaders(module_path, old_loaders)
            raise errors.FailedModuleLoad from exc
        else:
            modules_dict[module_path] = module

    def reload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = module_path.absolute()

            generator = self._reload_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = load_module()

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

        return self

    async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        loop = asyncio.get_running_loop()
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = await loop.run_in_executor(None, module_path.absolute)

            generator = self._reload_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = await loop.run_in_executor(None, load_module)

            try:
                generator.send(module)

            except StopIteration:
                pass

            else:
                raise RuntimeError("Generator didn't finish")

    async def on_message_create_event(self, event: hikari.MessageCreateEvent, /) -> None:
        """Execute a message command based on a gateway event.

        Parameters
        ----------
        hikari.events.message_events.MessageCreateEvent
            The event to handle.
        """
        if event.message.content is None:
            return

        ctx = self._make_message_context(
            client=self, injection_client=self, content=event.message.content, message=event.message
        )
        if (prefix := await self._check_prefix(ctx)) is None:
            return

        ctx.set_content(ctx.content.lstrip()[len(prefix) :].lstrip()).set_triggering_prefix(prefix)
        hooks: typing.Optional[set[tanjun_abc.MessageHooks]] = None
        if self._hooks and self._message_hooks:
            hooks = {self._hooks, self._message_hooks}

        elif self._hooks:
            hooks = {self._hooks}

        elif self._message_hooks:
            hooks = {self._message_hooks}

        try:
            if await self.check(ctx):
                for component in self._components.values():
                    if await component.execute_message(ctx, hooks=hooks):
                        return

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            return

        await self.dispatch_client_callback(ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND, ctx)

    def _get_slash_hooks(self) -> typing.Optional[set[tanjun_abc.SlashHooks]]:
        hooks: typing.Optional[set[tanjun_abc.SlashHooks]] = None
        if self._hooks and self._slash_hooks:
            hooks = {self._hooks, self._slash_hooks}

        elif self._hooks:
            hooks = {self._hooks}

        elif self._slash_hooks:
            hooks = {self._slash_hooks}

        return hooks

    async def _on_slash_not_found(self, ctx: context.SlashContext) -> None:
        await self.dispatch_client_callback(ClientCallbackNames.SLASH_COMMAND_NOT_FOUND, ctx)
        if self._interaction_not_found and not ctx.has_responded:
            await ctx.create_initial_response(self._interaction_not_found)

    async def on_interaction_create_event(self, event: hikari.InteractionCreateEvent, /) -> None:
        """Execute a slash command based on Gateway events.

        .. note::
            Any event where `event.interaction` is not
            `hikari.CommandInteraction` will be ignored.

        Parameters
        ----------
        event : hikari.events.interaction_events.InteractionCreateEvent
            The event to execute commands based on.
        """
        if not isinstance(event.interaction, hikari.CommandInteraction):
            return

        ctx = self._make_slash_context(
            client=self,
            injection_client=self,
            interaction=event.interaction,
            on_not_found=self._on_slash_not_found,
            default_to_ephemeral=self._defaults_to_ephemeral,
        )
        hooks = self._get_slash_hooks()

        if self._auto_defer_after is not None:
            ctx.start_defer_timer(self._auto_defer_after)

        try:
            if await self.check(ctx):
                for component in self._components.values():
                    # This is set on each iteration to ensure that any component
                    # state which was set to this isn't propagated to other components.
                    ctx.set_ephemeral_default(self._defaults_to_ephemeral)
                    if future := await component.execute_interaction(ctx, hooks=hooks):
                        await future
                        return

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            return

        await ctx.mark_not_found()

    async def on_interaction_create_request(self, interaction: hikari.CommandInteraction, /) -> context.ResponseTypeT:
        """Execute a slash command based on received REST requests.

        Parameters
        ----------
        interaction : hikari.CommandInteraction
            The interaction to execute a command based on.

        Returns
        -------
        tanjun.context.ResponseType
            The initial response to send back to Discord.
        """
        ctx = self._make_slash_context(
            client=self,
            injection_client=self,
            interaction=interaction,
            on_not_found=self._on_slash_not_found,
            default_to_ephemeral=self._defaults_to_ephemeral,
        )
        if self._auto_defer_after is not None:
            ctx.start_defer_timer(self._auto_defer_after)

        hooks = self._get_slash_hooks()
        future = ctx.get_response_future()
        try:
            if await self.check(ctx):
                for component in self._components.values():
                    # This is set on each iteration to ensure that any component
                    # state which was set to this isn't propagated to other components.
                    ctx.set_ephemeral_default(self._defaults_to_ephemeral)
                    if await component.execute_interaction(ctx, hooks=hooks):
                        return await future

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            # Under very specific timing there may be another future which could set a result while we await
            # ctx.respond therefore we create a task to avoid any erroneous behaviour from this trying to create
            # another response before it's returned the initial response.
            asyncio.get_running_loop().create_task(
                ctx.respond(exc.message), name=f"{interaction.id} command error responder"
            )
            return await future

        asyncio.get_running_loop().create_task(ctx.mark_not_found(), name=f"{interaction.id} not found")
        return await future


def _get_loaders(
    module: types.ModuleType, module_path: typing.Union[str, pathlib.Path], /
) -> list[tanjun_abc.ClientLoader]:
    exported = getattr(module, "__all__", None)
    if exported is not None and isinstance(exported, collections.Iterable):
        _LOGGER.debug("Scanning %s module based on its declared __all__)", module_path)
        exported = typing.cast("collections.Iterable[typing.Any]", exported)
        iterator = (getattr(module, name, None) for name in exported if isinstance(name, str))

    else:
        _LOGGER.debug("Scanning all public members on %s", module_path)
        iterator = (
            member
            for name, member in inspect.getmembers(module)
            if not name.startswith("_") or name.startswith("__") and name.endswith("__")
        )

    return [value for value in iterator if isinstance(value, tanjun_abc.ClientLoader)]


def _get_path_module(module_path: pathlib.Path, /) -> types.ModuleType:
    module_name = module_path.name.rsplit(".", 1)[0]
    spec = importlib_util.spec_from_file_location(module_name, module_path)

    # https://github.com/python/typeshed/issues/2793
    if not spec or not isinstance(spec.loader, importlib_abc.Loader):
        raise ModuleNotFoundError(f"Module not found at {module_path}", name=module_name, path=str(module_path))

    module = importlib_util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


class _WrapLoadError:
    __slots__ = ("_error",)

    def __init__(self, error: collections.Callable[[], Exception], /) -> None:
        self._error = error

    def __enter__(self) -> None:
        pass

    def __exit__(
        self,
        exc_type: typing.Optional[type[Exception]],
        exc: typing.Optional[Exception],
        exc_tb: typing.Optional[types.TracebackType],
    ) -> None:
        if exc and not isinstance(exc, errors.ModuleMissingLoaders):
            raise self._error() from exc  # noqa: R102 unnecessary parenthesis on raised exception

Standard Tanjun client.

#   def as_loader( callback: Union[collections.abc.Callable[[tanjun.clients.Client], None], collections.abc.Callable[[tanjun.abc.Client], None]], /, *, standard_impl: bool = True ) -> Union[collections.abc.Callable[[tanjun.clients.Client], None], collections.abc.Callable[[tanjun.abc.Client], None]]:
View Source
def as_loader(
    callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]],
    /,
    *,
    standard_impl: bool = True,
) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]:
    """Mark a callback as being used to load Tanjun components from a module.

    .. note::
        This is only necessary if you wish to use `tanjun.Client.load_modules`.

    Parameters
    ----------
    callback : collections.abc.Callable[[tanjun.abc.Client], None]]
        The callback used to load Tanjun components from a module.

        This should take one argument of type `Client` (or `tanjun.abc.Client`
        if `standard_impl` is `False`), return nothing and will be expected
        to initiate and add utilities such as components to the provided client.
    standard_impl : bool
        Whether this loader should only allow instances of `Client` as opposed
        to `tanjun.abc.Client`.

        Defaults to `True`.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.Client], None]]
        The decorated load callback.
    """
    return _LoaderDescriptor(callback, standard_impl)

Mark a callback as being used to load Tanjun components from a module.

Note: This is only necessary if you wish to use tanjun.Client.load_modules.

Parameters
  • callback (collections.abc.Callable[[tanjun.abc.Client], None]]): The callback used to load Tanjun components from a module.

    This should take one argument of type Client (or tanjun.abc.Client if standard_impl is False), return nothing and will be expected to initiate and add utilities such as components to the provided client.

  • standard_impl (bool): Whether this loader should only allow instances of Client as opposed to tanjun.abc.Client.

    Defaults to True.

Returns
#   def as_unloader( callback: Union[collections.abc.Callable[[tanjun.clients.Client], None], collections.abc.Callable[[tanjun.abc.Client], None]], /, *, standard_impl: bool = True ) -> Union[collections.abc.Callable[[tanjun.clients.Client], None], collections.abc.Callable[[tanjun.abc.Client], None]]:
View Source
def as_unloader(
    callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]],
    /,
    *,
    standard_impl: bool = True,
) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]:
    """Mark a callback as being used to unload a module's utilities from a client.

    .. note::
        This is the inverse of `as_loader` and is only necessary if you wish
        to use the `tanjun.Client.unload_module` or
        `tanjun.Client.reload_module`.

    Parameters
    ----------
    callback : collections.abc.Callable[[tanjun.Client], None]]
        The callback used to unload Tanjun components from a module.

        This should take one argument of type `Client` (or `tanjun.abc.Client`
        if `standard_impl` is `False`), return nothing and will be expected
        to remove utilities such as components from the provided client.
    standard_impl : bool
        Whether this unloader should only allow instances of `Client` as
        opposed to `tanjun.abc.Client`.

        Defaults to `True`.

    Returns
    -------
    collections.abc.Callable[[tanjun.Client], None]]
        The decorated unload callback.
    """
    return _UnloaderDescriptor(callback, standard_impl)

Mark a callback as being used to unload a module's utilities from a client.

Note: This is the inverse of as_loader and is only necessary if you wish to use the tanjun.Client.unload_module or tanjun.Client.reload_module.

Parameters
  • callback (collections.abc.Callable[[tanjun.Client], None]]): The callback used to unload Tanjun components from a module.

    This should take one argument of type Client (or tanjun.abc.Client if standard_impl is False), return nothing and will be expected to remove utilities such as components from the provided client.

  • standard_impl (bool): Whether this unloader should only allow instances of Client as opposed to tanjun.abc.Client.

    Defaults to True.

Returns
  • collections.abc.Callable[[tanjun.Client], None]]: The decorated unload callback.
View Source
class Client(injecting.InjectorClient, tanjun_abc.Client):
    """Tanjun's standard `tanjun.abc.Client` implementation.

    This implementation supports dependency injection for checks, command
    callbacks, prefix getters and event listeners. For more information on how
    this works see `tanjun.injecting`.

    .. note::
        By default this client includes a parser error handling hook which will
        by overwritten if you call `Client.set_hooks`.
    """

    __slots__ = (
        "_accepts",
        "_auto_defer_after",
        "_cache",
        "_cached_application_id",
        "_checks",
        "_client_callbacks",
        "_components",
        "_defaults_to_ephemeral",
        "_make_message_context",
        "_make_slash_context",
        "_events",
        "_grab_mention_prefix",
        "_hooks",
        "_interaction_not_found",
        "_slash_hooks",
        "_is_closing",
        "_listeners",
        "_loop",
        "_message_hooks",
        "_metadata",
        "_modules",
        "_path_modules",
        "_prefix_getter",
        "_prefixes",
        "_rest",
        "_server",
        "_shards",
        "_voice",
    )

    def __init__(
        self,
        rest: hikari.api.RESTClient,
        *,
        cache: typing.Optional[hikari.api.Cache] = None,
        events: typing.Optional[hikari.api.EventManager] = None,
        server: typing.Optional[hikari.api.InteractionServer] = None,
        shards: typing.Optional[hikari_traits.ShardAware] = None,
        voice: typing.Optional[hikari.api.VoiceComponent] = None,
        event_managed: bool = False,
        mention_prefix: bool = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        _stack_level: int = 0,
    ) -> None:
        """Initialise a Tanjun client.

        Notes
        -----
        * For a quicker way to initiate this client around a standard bot aware
        client, see `Client.from_gateway_bot` and `Client.from_rest_bot`.
        * The endpoint used by `declare_global_commands` has a strict ratelimit which,
        as of writing, only allows for 2 requests per minute (with that ratelimit
        either being per-guild if targeting a specific guild otherwise globally).
        * `event_manager` is necessary for message command dispatch and will also
        be necessary for interaction command dispatch if `server` isn't
        provided.
        * `server` is used for interaction command dispatch if interaction
        events aren't being received from the event manager.

        Parameters
        ----------
        rest : hikari.api.rest.RestClient
            The Hikari REST client this will use.

        Other Parameters
        ----------------
        cache : hikari.api.cache.CacheClient
            The Hikari cache client this will use if applicable.
        event_manager : hikari.api.event_manager.EventManagerClient
            The Hikari event manager client this will use if applicable.
        server : hikari.api.interaction_server.InteractionServer
            The Hikari interaction server client this will use if applicable.
        shards : hikari.traits.ShardAware
            The Hikari shard aware client this will use if applicable.
        voice : hikari.api.voice.VoiceComponent
            The Hikari voice component this will use if applicable.
        event_managed : bool
            Whether or not this client is managed by the event manager.

            An event managed client will be automatically started and closed based
            on Hikari's lifetime events.

            Defaults to `False` and can only be passed as `True` if `event_manager`
            is also provided.
        mention_prefix : bool
            Whether or not mention prefixes should be automatically set when this
            client is first started.

            Defaults to `False` and it should be noted that this only applies to
            message commands.
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.

        Raises
        ------
        ValueError
            Raises for the following reasons:
            * If `event_managed` is `True` when `event_manager` is `None`.
            * If `command_ids` is passed when multiple guild ids are provided for `declare_global_commands`.
            * If `command_ids` is passed when `declare_global_commands` is `False`.
        """  # noqa: E501 - line too long
        # InjectorClient.__init__
        super().__init__()
        if _LOGGER.isEnabledFor(logging.INFO):
            _LOGGER.info(
                "%s initialised with the following components: %s",
                "Event-managed client" if event_managed else "Client",
                ", ".join(
                    name
                    for name, value in [
                        ("cache", cache),
                        ("event manager", events),
                        ("interaction server", server),
                        ("rest", rest),
                        ("shard manager", shards),
                    ]
                    if value
                ),
            )

        if not events and not server:
            _LOGGER.warning(
                "Client initiaited without an event manager or interaction server, "
                "automatic command dispatch will be unavailable."
            )

        self._accepts = MessageAcceptsEnum.ALL if events else MessageAcceptsEnum.NONE
        self._auto_defer_after: typing.Optional[float] = 2.0
        self._cache = cache
        self._cached_application_id: typing.Optional[hikari.Snowflake] = None
        self._checks: list[checks.InjectableCheck] = []
        self._client_callbacks: dict[str, list[injecting.CallbackDescriptor[None]]] = {}
        self._components: dict[str, tanjun_abc.Component] = {}
        self._defaults_to_ephemeral: bool = False
        self._make_message_context: _MessageContextMakerProto = context.MessageContext
        self._make_slash_context: _SlashContextMakerProto = context.SlashContext
        self._events = events
        self._grab_mention_prefix = mention_prefix
        self._hooks: typing.Optional[tanjun_abc.AnyHooks] = hooks.AnyHooks().set_on_parser_error(on_parser_error)
        self._interaction_not_found: typing.Optional[str] = "Command not found"
        self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None
        self._is_closing = False
        self._listeners: dict[type[hikari.Event], list[injecting.SelfInjectingCallback[None]]] = {}
        self._loop: typing.Optional[asyncio.AbstractEventLoop] = None
        self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None
        self._metadata: dict[typing.Any, typing.Any] = {}
        self._modules: dict[str, types.ModuleType] = {}
        self._path_modules: dict[pathlib.Path, types.ModuleType] = {}
        self._prefix_getter: typing.Optional[injecting.CallbackDescriptor[collections.Iterable[str]]] = None
        self._prefixes: list[str] = []
        self._rest = rest
        self._server = server
        self._shards = shards
        self._voice = voice

        if event_managed:
            if not events:
                raise ValueError("Client cannot be event managed without an event manager")

            events.subscribe(hikari.StartingEvent, self._on_starting_event)
            events.subscribe(hikari.StoppingEvent, self._on_stopping_event)

        if set_global_commands:
            warnings.warn(
                "The `set_global_commands` argument is deprecated since v2.1.1a1. "
                "Use `declare_global_commands` instead.",
                DeprecationWarning,
                stacklevel=2 + _stack_level,
            )

        declare_global_commands = declare_global_commands or set_global_commands
        command_ids = command_ids or {}
        if isinstance(declare_global_commands, collections.Sequence):
            if command_ids and len(declare_global_commands) > 1:
                raise ValueError(
                    "Cannot provide specific command_ids while automatically "
                    "declaring commands marked as 'global' in multiple-guilds on startup"
                )

            for guild in declare_global_commands:
                _LOGGER.info("Registering startup command declarer for %s guild", guild)
                self.add_client_callback(ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, guild))

        elif isinstance(declare_global_commands, bool):
            if declare_global_commands:
                _LOGGER.info("Registering startup command declarer for global commands")
                if not command_ids:
                    _LOGGER.warning(
                        "No command IDs passed for startup command declarer, this could lead to previously set "
                        "command permissions being lost when commands are renamed."
                    )

                self.add_client_callback(
                    ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, hikari.UNDEFINED)
                )

            elif command_ids:
                raise ValueError("Cannot pass command IDs when not declaring global commands")

        else:
            self.add_client_callback(
                ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, declare_global_commands)
            )

        (
            self.set_type_dependency(tanjun_abc.Client, self)
            .set_type_dependency(Client, self)
            .set_type_dependency(type(self), self)
            .set_type_dependency(hikari.api.RESTClient, rest)
            .set_type_dependency(type(rest), rest)
        )
        if cache:
            self.set_type_dependency(hikari.api.Cache, cache).set_type_dependency(type(cache), cache)

        if events:
            self.set_type_dependency(hikari.api.EventManager, events).set_type_dependency(type(events), events)

        if server:
            self.set_type_dependency(hikari.api.InteractionServer, server).set_type_dependency(type(server), server)

        if shards:
            self.set_type_dependency(hikari_traits.ShardAware, shards).set_type_dependency(type(shards), shards)

        if voice:
            self.set_type_dependency(hikari.api.VoiceComponent, voice).set_type_dependency(type(voice), voice)

        dependencies.set_standard_dependencies(self)

    @classmethod
    def from_gateway_bot(
        cls,
        bot: hikari_traits.GatewayBotAware,
        /,
        *,
        event_managed: bool = True,
        mention_prefix: bool = False,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
    ) -> Client:
        """Build a `Client` from a `hikari.traits.GatewayBotAware` instance.

        Notes
        -----
        * This implicitly defaults the client to human only mode.
        * This sets type dependency injectors for the hikari traits present in
          `bot` (including `hikari.traits.GatewayBotAware`).
        * The endpoint used by `declare_global_commands` has a strict ratelimit
          which, as of writing, only allows for 2 requests per minute (with that
          ratelimit either being per-guild if targeting a specific guild
          otherwise globally).

        Parameters
        ----------
        bot : hikari.traits.GatewayBotAware
            The bot client to build from.

            This will be used to infer the relevant Hikari clients to use.

        Other Parameters
        ----------------
        event_managed : bool
            Whether or not this client is managed by the event manager.

            An event managed client will be automatically started and closed
            based on Hikari's lifetime events.

            Defaults to `True`.
        mention_prefix : bool
            Whether or not mention prefixes should be automatically set when this
            client is first started.

            Defaults to `False` and it should be noted that this only applies to
            message commands.
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.
        """  # noqa: E501 - line too long
        return (
            cls(
                rest=bot.rest,
                cache=bot.cache,
                events=bot.event_manager,
                shards=bot,
                voice=bot.voice,
                event_managed=event_managed,
                mention_prefix=mention_prefix,
                declare_global_commands=declare_global_commands,
                set_global_commands=set_global_commands,
                command_ids=command_ids,
                _stack_level=1,
            )
            .set_human_only()
            .set_hikari_trait_injectors(bot)
        )

    @classmethod
    def from_rest_bot(
        cls,
        bot: hikari_traits.RESTBotAware,
        /,
        *,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
    ) -> Client:
        """Build a `Client` from a `hikari.traits.RESTBotAware` instance.

        Notes
        -----
        * This sets type dependency injectors for the hikari traits present in
          `bot` (including `hikari.traits.RESTBotAware`).
        * The endpoint used by `declare_global_commands` has a strict ratelimit
          which, as of writing, only allows for 2 requests per minute (with that
          ratelimit either being per-guild if targeting a specific guild
          otherwise globally).

        Parameters
        ----------
        bot : hikari.traits.RESTBotAware
            The bot client to build from.

        Other Parameters
        ----------------
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.
        """  # noqa: E501 - line too long
        return cls(
            rest=bot.rest,
            server=bot.interaction_server,
            declare_global_commands=declare_global_commands,
            set_global_commands=set_global_commands,
            command_ids=command_ids,
            _stack_level=1,
        ).set_hikari_trait_injectors(bot)

    async def __aenter__(self) -> Client:
        await self.open()
        return self

    async def __aexit__(
        self,
        exc_type: typing.Optional[type[Exception]],
        exc: typing.Optional[Exception],
        exc_traceback: typing.Optional[types.TracebackType],
    ) -> None:
        await self.close()

    def __repr__(self) -> str:
        return f"CommandClient <{type(self).__name__!r}, {len(self._components)} components, {self._prefixes}>"

    @property
    def defaults_to_ephemeral(self) -> bool:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._defaults_to_ephemeral

    @property
    def message_accepts(self) -> MessageAcceptsEnum:
        """Type of message create events this command client accepts for execution."""
        return self._accepts

    @property
    def is_human_only(self) -> bool:
        """Whether this client is only executing for non-bot/webhook users messages."""
        return typing.cast("checks.InjectableCheck", _check_human) in self._checks

    @property
    def cache(self) -> typing.Optional[hikari.api.Cache]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._cache

    @property
    def checks(self) -> collections.Collection[tanjun_abc.CheckSig]:
        """Collection of the level `tanjun.abc.Context` checks registered to this client.

        .. note::
            These may be taking advantage of the standard dependency injection.
        """
        return tuple(check.callback for check in self._checks)

    @property
    def components(self) -> collections.Collection[tanjun_abc.Component]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._components.copy().values()

    @property
    def events(self) -> typing.Optional[hikari.api.EventManager]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._events

    @property
    def listeners(
        self,
    ) -> collections.Mapping[type[hikari.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]:
        return utilities.CastedView(
            self._listeners,
            lambda x: [typing.cast(tanjun_abc.ListenerCallbackSig, callback.callback) for callback in x],
        )

    @property
    def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]:
        """Top level `tanjun.abc.AnyHooks` set for this client.

        These are called during both message and interaction command execution.

        Returns
        -------
        typing.Optional[tanjun.abc.AnyHooks]
            The top level `tanjun.abc.Context` based hooks set for this
            client if applicable, else `None`.
        """
        return self._hooks

    @property
    def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]:
        """Top level `tanjun.abc.SlashHooks` set for this client.

        These are only called during interaction command execution.
        """
        return self._slash_hooks

    @property
    def is_alive(self) -> bool:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._loop is not None

    @property
    def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._loop

    @property
    def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]:
        """Top level `tanjun.abc.MessageHooks` set for this client.

        These are only called during both message command execution.
        """
        return self._message_hooks

    @property
    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._metadata

    @property
    def prefix_getter(self) -> typing.Optional[PrefixGetterSig]:
        """Prefix getter method set for this client.

        For more information on this callback's signature see `PrefixGetter`.
        """
        return typing.cast(PrefixGetterSig, self._prefix_getter.callback) if self._prefix_getter else None

    @property
    def prefixes(self) -> collections.Collection[str]:
        """Collection of the standard prefixes set for this client."""
        return self._prefixes.copy()

    @property
    def rest(self) -> hikari.api.RESTClient:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._rest

    @property
    def server(self) -> typing.Optional[hikari.api.InteractionServer]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._server

    @property
    def shards(self) -> typing.Optional[hikari_traits.ShardAware]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._shards

    @property
    def voice(self) -> typing.Optional[hikari.api.VoiceComponent]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._voice

    async def _on_starting_event(self, _: hikari.StartingEvent, /) -> None:
        await self.open()

    async def _on_stopping_event(self, _: hikari.StoppingEvent, /) -> None:
        await self.close()

    async def clear_application_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        if application is None:
            application = self._cached_application_id or await self.fetch_rest_application_id()

        await self._rest.set_application_commands(application, (), guild=guild)

    async def set_global_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        """Alias of `Client.declare_global_commands`.

        .. deprecated:: v2.1.1a1
            Use `Client.declare_global_commands` instead.
        """
        warnings.warn(
            "The `Client.set_global_commands` method has been deprecated since v2.1.1a1. "
            "Use `Client.declare_global_commands` instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return await self.declare_global_commands(application=application, guild=guild, force=force)

    async def declare_global_commands(
        self,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        # <<inherited docstring from tanjun.abc.Client>>.
        commands = (
            command
            for command in itertools.chain.from_iterable(
                component.slash_commands for component in self._components.values()
            )
            if command.is_global
        )
        return await self.declare_application_commands(
            commands, command_ids, application=application, guild=guild, force=force
        )

    async def declare_application_command(
        self,
        command: tanjun_abc.BaseSlashCommand,
        /,
        command_id: typing.Optional[hikari.Snowflakeish] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> hikari.Command:
        # <<inherited docstring from tanjun.abc.Client>>.
        builder = command.build()
        if command_id:
            response = await self._rest.edit_application_command(
                application or self._cached_application_id or await self.fetch_rest_application_id(),
                command_id,
                guild=guild,
                name=builder.name,
                description=builder.description,
                options=builder.options,
            )

        else:
            response = await self._rest.create_application_command(
                application or self._cached_application_id or await self.fetch_rest_application_id(),
                guild=guild,
                name=builder.name,
                description=builder.description,
                options=builder.options,
            )

        if not guild:
            command.set_tracked_command(response)  # TODO: is this fine?

        return response

    async def declare_application_commands(
        self,
        commands: collections.Iterable[tanjun_abc.BaseSlashCommand],
        /,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        # <<inherited docstring from tanjun.abc.Client>>.
        command_ids = command_ids or {}
        names_to_commands: dict[str, tanjun_abc.BaseSlashCommand] = {}
        conflicts: set[str] = set()
        builders: dict[str, hikari.api.CommandBuilder] = {}

        for command in commands:
            names_to_commands[command.name] = command
            if command.name in builders:
                conflicts.add(command.name)

            builder = command.build()
            if command_id := command_ids.get(command.name):
                builder.set_id(hikari.Snowflake(command_id))

            builders[command.name] = builder

        if conflicts:
            raise ValueError(
                "Couldn't declare commands due to conflicts. The following command names have more than one command "
                "registered for them " + ", ".join(conflicts)
            )

        if len(builders) > 100:
            raise ValueError("You can only declare up to 100 top level commands in a guild or globally")

        if not application:
            application = self._cached_application_id or await self.fetch_rest_application_id()

        target_type = "global" if guild is hikari.UNDEFINED else f"guild {int(guild)}"

        if not force:
            registered_commands = await self._rest.fetch_application_commands(application, guild=guild)
            if len(registered_commands) == len(builders) and all(
                _cmp_command(builders.get(command.name), command) for command in registered_commands
            ):
                _LOGGER.info("Skipping bulk declare for %s slash commands since they're already declared", target_type)
                return registered_commands

        _LOGGER.info("Bulk declaring %s %s slash commands", len(builders), target_type)
        responses = await self._rest.set_application_commands(application, list(builders.values()), guild=guild)

        for response in responses:
            if not guild:
                names_to_commands[response.name].set_tracked_command(response)  # TODO: is this fine?

            if (expected_id := command_ids.get(response.name)) and hikari.Snowflake(expected_id) != response.id:
                _LOGGER.warning(
                    "ID mismatch found for %s command %r, expected %s but got %s. "
                    "This suggests that any previous permissions set for this command will have been lost.",
                    target_type,
                    response.name,
                    expected_id,
                    response.id,
                )

        _LOGGER.info("Successfully declared %s (top-level) %s commands", len(responses), target_type)
        if _LOGGER.isEnabledFor(logging.DEBUG):
            _LOGGER.debug(
                "Declared %s command ids; %s",
                target_type,
                ", ".join(f"{response.name}: {response.id}" for response in responses),
            )

        return responses

    def set_auto_defer_after(self: _ClientT, time: typing.Optional[float], /) -> _ClientT:
        """Set when this client should automatically defer execution of commands.

        .. warning::
            If `time` is set to `None` then automatic deferrals will be disabled.
            This may lead to unexpected behaviour.

        Parameters
        ----------
        time : typing.Optional[float]
            The time in seconds to defer interaction command responses after.
        """
        self._auto_defer_after = float(time) if time is not None else None
        return self

    def set_ephemeral_default(self: _ClientT, state: bool, /) -> _ClientT:
        """Set whether slash contexts spawned by this client should default to ephemeral responses.

        Parameters
        ----------
        bool
            Whether slash command contexts executed in this component should
            should default to ephemeral.

            This will be overridden by any response calls which specify flags
            and defaults to `False`.

        Returns
        -------
        SelfT
            This component to enable method chaining.
        """
        self._defaults_to_ephemeral = state
        return self

    def set_hikari_trait_injectors(self: _ClientT, bot: hikari_traits.RESTAware, /) -> _ClientT:
        """Set type based dependency injection based on the hikari traits found in `bot`.

        This is a short hand for calling `Client.add_type_dependency` for all
        the hikari trait types `bot` is valid for with bot.

        Parameters
        ----------
        bot : hikari_traits.RESTAware
            The hikari client to set dependency injectors for.
        """
        for _, member in inspect.getmembers(hikari_traits):
            if inspect.isclass(member) and isinstance(bot, member):
                self.set_type_dependency(member, bot)

        return self

    def set_interaction_not_found(self: _ClientT, message: typing.Optional[str], /) -> _ClientT:
        """Set the response message for when an interaction command is not found.

        .. warning::
            Setting this to `None` may lead to unexpected behaviour (especially
            when the client is still set to auto-defer interactions) and should
            only be done if you know what you're doing.

        Parameters
        ----------
        message : typing.Optional[str]
            The message to respond with when an interaction command isn't found.
        """
        self._interaction_not_found = message
        return self

    def set_message_accepts(self: _ClientT, accepts: MessageAcceptsEnum, /) -> _ClientT:
        """Set the kind of messages commands should be executed based on.

        Parameters
        ----------
        accepts : MessageAcceptsEnum
            The type of messages commands should be executed based on.
        """
        if accepts.get_event_type() and not self._events:
            raise ValueError("Cannot set accepts level on a client with no event manager")

        self._accepts = accepts
        return self

    def set_message_ctx_maker(self: _ClientT, maker: _MessageContextMakerProto = context.MessageContext, /) -> _ClientT:
        """Set the message context maker to use when creating context for a message.

        .. warning::
            The caller must return an instance of `tanjun.context.MessageContext`
            rather than just any implementation of the MessageContext abc due to
            this client relying on implementation detail of
            `tanjun.context.MessageContext`.

        Parameters
        ----------
        maker : _MessageContextMakerProto
            The message context maker to use.

            This is a callback which should match the signature of
            `tanjun.context.MessageContext.__init__` and return an instance
            of `tanjun.context.MessageContext`.

            This defaults to `tanjun.context.MessageContext`.
        """
        self._make_message_context = maker
        return self

    def set_metadata(self: _ClientT, key: typing.Any, value: typing.Any, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        self._metadata[key] = value
        return self

    def set_slash_ctx_maker(self: _ClientT, maker: _SlashContextMakerProto = context.SlashContext, /) -> _ClientT:
        """Set the slash context maker to use when creating context for a slash command.

        .. warning::
            The caller must return an instance of `tanjun.context.SlashContext`
            rather than just any implementation of the SlashContext abc due to
            this client relying on implementation detail of
            `tanjun.context.SlashContext`.

        Parameters
        ----------
        maker : _SlashContextMakerProto
            The slash context maker to use.

            This is a callback which should match the signature of
            `tanjun.context.SlashContext.__init__` and return an instance
            of `tanjun.context.SlashContext`.

            This defaults to `tanjun.context.SlashContext`.
        """
        self._make_slash_context = maker
        return self

    def set_human_only(self: _ClientT, value: bool = True) -> _ClientT:
        """Set whether or not message commands execution should be limited to "human" users.

        .. note::
            This doesn't apply to interaction commands as these can only be
            triggered by a "human" (normal user account).

        Parameters
        ----------
        value : bool
            Whether or not message commands execution should be limited to "human" users.

            Passing `True` here will prevent message commands from being executed
            based on webhook and bot messages.
        """
        if value:
            self.add_check(_check_human)

        else:
            try:
                self.remove_check(_check_human)
            except ValueError:
                pass

        return self

    def add_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT:
        """Add a generic check to this client.

        This will be applied to both message and slash command execution.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to add. This may be either synchronous or asynchronous
            and must take one positional argument of type `tanjun.abc.Context`
            with dependency injection being supported for its keyword arguments.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        if check not in self._checks:
            self._checks.append(checks.InjectableCheck(check))

        return self

    def remove_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT:
        """Remove a check from the client.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to remove.

        Raises
        ------
        ValueError
            If the check was not previously added.
        """
        self._checks.remove(typing.cast("checks.InjectableCheck", check))
        return self

    def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT:
        """Add a check to this client through a decorator call.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to add. This may be either synchronous or asynchronous
            and must take one positional argument of type `tanjun.abc.Context`
            with dependency injection being supported for its keyword arguments.

        Returns
        -------
        tanjun_abc.CheckSig
            The added check.
        """
        self.add_check(check)
        return check

    async def check(self, ctx: tanjun_abc.Context, /) -> bool:
        return await utilities.gather_checks(ctx, self._checks)

    def add_component(self: _ClientT, component: tanjun_abc.Component, /, *, add_injector: bool = False) -> _ClientT:
        """Add a component to this client.

        Parameters
        ----------
        component: Component
            The component to move to this client.

        Returns
        -------
        Self
            The client instance to allow chained calls.

        Raises
        ------
        ValueError
            If the component's name is already registered.
        """
        if component.name in self._components:
            raise ValueError(f"A component named {component.name!r} is already registered.")

        component.bind_client(self)
        self._components[component.name] = component

        if add_injector:
            self.set_type_dependency(type(component), lambda: component)

        if self._loop:
            self._loop.create_task(component.open())
            self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.COMPONENT_ADDED, component))

        return self

    def get_component_by_name(self, name: str, /) -> typing.Optional[tanjun_abc.Component]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._components.get(name)

    def remove_component(self: _ClientT, component: tanjun_abc.Component, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        stored_component = self._components.get(component.name)
        if not stored_component or stored_component != component:
            raise ValueError(f"The component {component!r} is not registered.")

        del self._components[component.name]

        if self._loop:
            self._loop.create_task(component.close(unbind=True))
            self._loop.create_task(
                self.dispatch_client_callback(ClientCallbackNames.COMPONENT_REMOVED, stored_component)
            )

        else:
            stored_component.unbind_client(self)

        return self

    def remove_component_by_name(self: _ClientT, name: str, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self.remove_component(self._components[name])

    def add_client_callback(
        self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        descriptor = injecting.CallbackDescriptor(callback)
        name = name.casefold()
        try:
            if descriptor in self._client_callbacks[name]:
                return self

            self._client_callbacks[name].append(descriptor)
        except KeyError:
            self._client_callbacks[name] = [descriptor]

        return self

    async def dispatch_client_callback(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /, *args: typing.Any
    ) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        if callbacks := self._client_callbacks.get(name):
            calls = (
                _wrap_client_callback(callback, injecting.BasicInjectionContext(self), args) for callback in callbacks
            )
            await asyncio.gather(*calls)

    def get_client_callbacks(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Collection[tanjun_abc.MetaEventSig]:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        if result := self._client_callbacks.get(name):
            return tuple(callback.callback for callback in result)

        return ()

    def remove_client_callback(
        self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        self._client_callbacks[name].remove(typing.cast("injecting.CallbackDescriptor[None]", callback))
        if not self._client_callbacks[name]:
            del self._client_callbacks[name]

        return self

    def with_client_callback(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]:
        # <<inherited docstring from tanjun.abc.Client>>.
        def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT:
            self.add_client_callback(name, callback)
            return callback

        return decorator

    def add_listener(
        self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        injected: injecting.SelfInjectingCallback[None] = injecting.SelfInjectingCallback(self, callback)
        try:
            if callback in self._listeners[event_type]:
                return self

            self._listeners[event_type].append(injected)

        except KeyError:
            self._listeners[event_type] = [injected]

        if self._loop and self._events:
            self._events.subscribe(event_type, injected.__call__)

        return self

    def remove_listener(
        self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        index = self._listeners[event_type].index(typing.cast("injecting.SelfInjectingCallback[None]", callback))
        registered_callback = self._listeners[event_type].pop(index)

        if not self._listeners[event_type]:
            del self._listeners[event_type]

        if self._loop and self._events:
            self._events.unsubscribe(event_type, registered_callback.__call__)

        return self

    def with_listener(
        self, event_type: type[hikari.Event], /
    ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]:
        # <<inherited docstring from tanjun.abc.Client>>.
        def decorator(callback: tanjun_abc.ListenerCallbackSigT, /) -> tanjun_abc.ListenerCallbackSigT:
            self.add_listener(event_type, callback)
            return callback

        return decorator

    def add_prefix(self: _ClientT, prefixes: typing.Union[collections.Iterable[str], str], /) -> _ClientT:
        """Add a prefix used to filter message command calls.

        This will be matched against the first character(s) in a message's
        content to determine whether the message command search stage of
        execution should be initiated.

        Parameters
        ----------
        prefixes : typing.Union[collections.abc.Iterable[str], str]
            Either a single string or an iterable of strings to be used as
            prefixes.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        if isinstance(prefixes, str):
            if prefixes not in self._prefixes:
                self._prefixes.append(prefixes)

        else:
            self._prefixes.extend(prefix for prefix in prefixes if prefix not in self._prefixes)

        return self

    def remove_prefix(self: _ClientT, prefix: str, /) -> _ClientT:
        """Remove a message content prefix from the client.

        Parameters
        ----------
        prefix : str
            The prefix to remove.

        Raises
        ------
        ValueError
            If the prefix is not registered with the client.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._prefixes.remove(prefix)
        return self

    def set_prefix_getter(self: _ClientT, getter: typing.Optional[PrefixGetterSig], /) -> _ClientT:
        """Set the callback used to retrieve message prefixes set for the relevant guild.

        Parameters
        ----------
        getter : typing.Optional[PrefixGetterSig]
            The callback which'll be used to retrieve prefixes for the guild a
            message context is from. If `None` is passed here then the callback
            will be unset.

            This should be an async callback which one argument of type
            `tanjun.abc.MessageContext` and returns an iterable of string prefixes.
            Dependency injection is supported for this callback's keyword arguments.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._prefix_getter = injecting.CallbackDescriptor(getter) if getter else None
        return self

    def with_prefix_getter(self, getter: PrefixGetterSigT, /) -> PrefixGetterSigT:
        """Set the prefix getter callback for this client through decorator call.

        Examples
        --------
        ```py
        client = tanjun.Client.from_rest_bot(bot)

        @client.with_prefix_getter
        async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]:
            raise NotImplementedError
        ```

        Parameters
        ----------
        getter : PrefixGetterSig
            The callback which'll be  to retrieve prefixes for the guild a
            message event is from.

            This should be an async callback which one argument of type
            `tanjun.abc.MessageContext` and returns an iterable of string prefixes.
            Dependency injection is supported for this callback's keyword arguments.

        Returns
        -------
        PrefixGetterSigT
            The registered callback.
        """
        self.set_prefix_getter(getter)
        return getter

    def iter_commands(self) -> collections.Iterator[tanjun_abc.ExecutableCommand[tanjun_abc.Context]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        slash_commands = self.iter_slash_commands(global_only=False)
        yield from self.iter_message_commands()
        yield from slash_commands

    def iter_message_commands(self) -> collections.Iterator[tanjun_abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(component.message_commands for component in self.components)

    def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Client>>.
        if global_only:
            return filter(lambda c: c.is_global, self.iter_slash_commands(global_only=False))

        return itertools.chain.from_iterable(component.slash_commands for component in self.components)

    def check_message_name(
        self, name: str, /
    ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(
            component.check_message_name(name) for component in self._components.values()
        )

    def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(
            component.check_slash_name(name) for component in self._components.values()
        )

    async def _check_prefix(self, ctx: tanjun_abc.MessageContext, /) -> typing.Optional[str]:
        if self._prefix_getter:
            for prefix in await self._prefix_getter.resolve_with_command_context(ctx, ctx):
                if ctx.content.startswith(prefix):
                    return prefix

        for prefix in self._prefixes:
            if ctx.content.startswith(prefix):
                return prefix

        return None

    def _try_unsubscribe(
        self,
        event_manager: hikari.api.EventManager,
        event_type: type[hikari.Event],
        callback: tanjun_abc.ListenerCallbackSig,
    ) -> None:
        try:
            event_manager.unsubscribe(event_type, callback)
        except (ValueError, LookupError):
            # TODO: add logging here
            pass

    async def close(self, *, deregister_listeners: bool = True) -> None:
        """Close the client.

        Raises
        ------
        RuntimeError
            If the client isn't running.
        """
        if not self._loop:
            raise RuntimeError("Client isn't active")

        if self._is_closing:
            event = asyncio.Event()
            self.add_client_callback(ClientCallbackNames.CLOSED, event.set)
            try:
                await event.wait()
            finally:
                self.remove_client_callback(ClientCallbackNames.CLOSED, event.set)
            return

        self._is_closing = True
        await self.dispatch_client_callback(ClientCallbackNames.CLOSING)
        if deregister_listeners and self._events:
            if event_type := self._accepts.get_event_type():
                self._try_unsubscribe(self._events, event_type, self.on_message_create_event)

            self._try_unsubscribe(self._events, hikari.InteractionCreateEvent, self.on_interaction_create_event)

            for event_type_, listeners in self._listeners.items():
                for listener in listeners:
                    self._try_unsubscribe(self._events, event_type_, listener.__call__)

        if deregister_listeners and self._server:
            self._server.set_listener(hikari.CommandInteraction, None)

        await asyncio.gather(*(component.close() for component in self._components.copy().values()))

        self._loop = None
        await self.dispatch_client_callback(ClientCallbackNames.CLOSED)
        self._is_closing = False

    async def open(self, *, register_listeners: bool = True) -> None:
        """Start the client.

        If `mention_prefix` was passed to `Client.__init__` or
        `Client.from_gateway_bot` then this function may make a fetch request
        to Discord if it cannot get the current user from the cache.

        Raises
        ------
        RuntimeError
            If the client is already active.
        """
        if self._loop:
            raise RuntimeError("Client is already alive")

        self._loop = asyncio.get_running_loop()
        self._is_closing = False
        await self.dispatch_client_callback(ClientCallbackNames.STARTING)

        if self._grab_mention_prefix:
            user: typing.Optional[hikari.OwnUser] = None
            if self._cache:
                user = self._cache.get_me()

            if not user and (user_cache := self.get_type_dependency(dependencies.SingleStoreCache[hikari.OwnUser])):
                user = await user_cache.get(default=None)

            if not user:
                user = await self._rest.fetch_my_user()

            for prefix in f"<@{user.id}>", f"<@!{user.id}>":
                if prefix not in self._prefixes:
                    self._prefixes.append(prefix)

            self._grab_mention_prefix = False

        await asyncio.gather(*(component.open() for component in self._components.copy().values()))

        if register_listeners and self._events:
            if event_type := self._accepts.get_event_type():
                self._events.subscribe(event_type, self.on_message_create_event)

            self._events.subscribe(hikari.InteractionCreateEvent, self.on_interaction_create_event)

            for event_type_, listeners in self._listeners.items():
                for listener in listeners:
                    self._events.subscribe(event_type_, listener.__call__)

        if register_listeners and self._server:
            self._server.set_listener(hikari.CommandInteraction, self.on_interaction_create_request)

        self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.STARTED))

    async def fetch_rest_application_id(self) -> hikari.Snowflake:
        """Fetch the ID of the application this client is linked to.

        Returns
        -------
        hikari.Snowflake
            The application ID of the application this client is linked to.
        """
        if self._cached_application_id:
            return self._cached_application_id

        application_cache = self.get_type_dependency(
            dependencies.SingleStoreCache[hikari.Application]
        ) or self.get_type_dependency(dependencies.SingleStoreCache[hikari.AuthorizationApplication])
        if application_cache and (application := await application_cache.get(default=None)):
            self._cached_application_id = application.id
            return application.id

        if self._rest.token_type == hikari.TokenType.BOT:
            self._cached_application_id = hikari.Snowflake(await self._rest.fetch_application())

        else:
            self._cached_application_id = hikari.Snowflake((await self._rest.fetch_authorization()).application)

        return self._cached_application_id

    def set_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ClientT:
        """Set the general command execution hooks for this client.

        The callbacks within this hook will be added to every slash and message
        command execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.AnyHooks]
            The general command execution hooks to set for this client.

            Passing `None` will remove all hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._hooks = hooks
        return self

    def set_slash_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.SlashHooks], /) -> _ClientT:
        """Set the slash command execution hooks for this client.

        The callbacks within this hook will be added to every slash command
        execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.SlashHooks]
            The slash context specific command execution hooks to set for this
            client.

            Passing `None` will remove the hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._slash_hooks = hooks
        return self

    def set_message_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.MessageHooks], /) -> _ClientT:
        """Set the message command execution hooks for this client.

        The callbacks within this hook will be added to every message command
        execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.MessageHooks]
            The message context specific command execution hooks to set for this
            client.

            Passing `None` will remove all hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._message_hooks = hooks
        return self

    def _call_loaders(
        self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], /
    ) -> None:
        found = False
        for loader in loaders:
            if loader.load(self):
                found = True

        if not found:
            raise errors.ModuleMissingLoaders(f"Didn't find any loaders in {module_path}", module_path)

    def _call_unloaders(
        self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], /
    ) -> None:
        found = False
        for loader in loaders:
            if loader.unload(self):
                found = True

        if not found:
            raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in {module_path}", module_path)

    def _load_module(
        self, module_path: typing.Union[str, pathlib.Path]
    ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]:
        if isinstance(module_path, str):
            if module_path in self._modules:
                raise errors.ModuleStateConflict(f"module {module_path} already loaded", module_path)

            _LOGGER.info("Loading from %s", module_path)
            module = yield lambda: importlib.import_module(module_path)

            with _WrapLoadError(errors.FailedModuleLoad):
                self._call_loaders(module_path, _get_loaders(module, module_path))

            self._modules[module_path] = module

        else:
            module_path_abs = module_path.absolute()
            if module_path_abs in self._path_modules:
                raise errors.ModuleStateConflict(f"Module at {module_path} already loaded", module_path)

            _LOGGER.info("Loading from %s", module_path)
            module = yield lambda: _get_path_module(module_path)

            with _WrapLoadError(errors.FailedModuleLoad):
                self._call_loaders(module_path, _get_loaders(module, module_path))

            self._path_modules[module_path_abs] = module

    def load_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = module_path.absolute()

            generator = self._load_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = load_module()

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

        return self

    async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        loop = asyncio.get_running_loop()
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = await loop.run_in_executor(None, module_path.absolute)

            generator = self._load_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = await loop.run_in_executor(None, load_module)

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

    def unload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.ab.Client>>.
        for module_path in modules:
            if isinstance(module_path, str):
                modules_dict: dict[typing.Any, types.ModuleType] = self._modules

            else:
                modules_dict = self._path_modules
                module_path = module_path.absolute()

            module = modules_dict.get(module_path)
            if not module:
                raise errors.ModuleStateConflict(f"Module {module_path!s} not loaded", module_path)

            _LOGGER.info("Unloading from %s", module_path)
            with _WrapLoadError(errors.FailedModuleUnload):
                self._call_unloaders(module_path, _get_loaders(module, module_path))

            del modules_dict[module_path]

        return self

    def _reload_module(
        self, module_path: typing.Union[str, pathlib.Path]
    ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]:
        if isinstance(module_path, str):
            old_module = self._modules.get(module_path)

            def load_module() -> types.ModuleType:
                assert old_module
                return importlib.reload(old_module)

            modules_dict: dict[typing.Any, types.ModuleType] = self._modules

        else:
            old_module = self._path_modules.get(module_path)

            def load_module() -> types.ModuleType:
                assert isinstance(module_path, pathlib.Path)
                return _get_path_module(module_path)

            modules_dict = self._path_modules

        if not old_module:
            raise errors.ModuleStateConflict(f"Module {module_path} not loaded", module_path)

        _LOGGER.info("Reloading %s", module_path)

        old_loaders = _get_loaders(old_module, module_path)
        # We assert that the old module has unloaders early to avoid unnecessarily
        # importing the new module.
        if not any(loader.has_unload for loader in old_loaders):
            raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in old {module_path}", module_path)

        module = yield load_module

        loaders = _get_loaders(module, module_path)

        # We assert that the new module has loaders early to avoid unnecessarily
        # unloading then rolling back when we know it's going to fail to load.
        if not any(loader.has_load for loader in loaders):
            raise errors.ModuleMissingLoaders(f"Didn't find any loaders in new {module_path}", module_path)

        with _WrapLoadError(errors.FailedModuleUnload):
            # This will never raise MissingLoaders as we assert this earlier
            self._call_unloaders(module_path, old_loaders)

        try:
            # This will never raise MissingLoaders as we assert this earlier
            self._call_loaders(module_path, loaders)
        except Exception as exc:
            self._call_loaders(module_path, old_loaders)
            raise errors.FailedModuleLoad from exc
        else:
            modules_dict[module_path] = module

    def reload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = module_path.absolute()

            generator = self._reload_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = load_module()

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

        return self

    async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        loop = asyncio.get_running_loop()
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = await loop.run_in_executor(None, module_path.absolute)

            generator = self._reload_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = await loop.run_in_executor(None, load_module)

            try:
                generator.send(module)

            except StopIteration:
                pass

            else:
                raise RuntimeError("Generator didn't finish")

    async def on_message_create_event(self, event: hikari.MessageCreateEvent, /) -> None:
        """Execute a message command based on a gateway event.

        Parameters
        ----------
        hikari.events.message_events.MessageCreateEvent
            The event to handle.
        """
        if event.message.content is None:
            return

        ctx = self._make_message_context(
            client=self, injection_client=self, content=event.message.content, message=event.message
        )
        if (prefix := await self._check_prefix(ctx)) is None:
            return

        ctx.set_content(ctx.content.lstrip()[len(prefix) :].lstrip()).set_triggering_prefix(prefix)
        hooks: typing.Optional[set[tanjun_abc.MessageHooks]] = None
        if self._hooks and self._message_hooks:
            hooks = {self._hooks, self._message_hooks}

        elif self._hooks:
            hooks = {self._hooks}

        elif self._message_hooks:
            hooks = {self._message_hooks}

        try:
            if await self.check(ctx):
                for component in self._components.values():
                    if await component.execute_message(ctx, hooks=hooks):
                        return

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            return

        await self.dispatch_client_callback(ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND, ctx)

    def _get_slash_hooks(self) -> typing.Optional[set[tanjun_abc.SlashHooks]]:
        hooks: typing.Optional[set[tanjun_abc.SlashHooks]] = None
        if self._hooks and self._slash_hooks:
            hooks = {self._hooks, self._slash_hooks}

        elif self._hooks:
            hooks = {self._hooks}

        elif self._slash_hooks:
            hooks = {self._slash_hooks}

        return hooks

    async def _on_slash_not_found(self, ctx: context.SlashContext) -> None:
        await self.dispatch_client_callback(ClientCallbackNames.SLASH_COMMAND_NOT_FOUND, ctx)
        if self._interaction_not_found and not ctx.has_responded:
            await ctx.create_initial_response(self._interaction_not_found)

    async def on_interaction_create_event(self, event: hikari.InteractionCreateEvent, /) -> None:
        """Execute a slash command based on Gateway events.

        .. note::
            Any event where `event.interaction` is not
            `hikari.CommandInteraction` will be ignored.

        Parameters
        ----------
        event : hikari.events.interaction_events.InteractionCreateEvent
            The event to execute commands based on.
        """
        if not isinstance(event.interaction, hikari.CommandInteraction):
            return

        ctx = self._make_slash_context(
            client=self,
            injection_client=self,
            interaction=event.interaction,
            on_not_found=self._on_slash_not_found,
            default_to_ephemeral=self._defaults_to_ephemeral,
        )
        hooks = self._get_slash_hooks()

        if self._auto_defer_after is not None:
            ctx.start_defer_timer(self._auto_defer_after)

        try:
            if await self.check(ctx):
                for component in self._components.values():
                    # This is set on each iteration to ensure that any component
                    # state which was set to this isn't propagated to other components.
                    ctx.set_ephemeral_default(self._defaults_to_ephemeral)
                    if future := await component.execute_interaction(ctx, hooks=hooks):
                        await future
                        return

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            return

        await ctx.mark_not_found()

    async def on_interaction_create_request(self, interaction: hikari.CommandInteraction, /) -> context.ResponseTypeT:
        """Execute a slash command based on received REST requests.

        Parameters
        ----------
        interaction : hikari.CommandInteraction
            The interaction to execute a command based on.

        Returns
        -------
        tanjun.context.ResponseType
            The initial response to send back to Discord.
        """
        ctx = self._make_slash_context(
            client=self,
            injection_client=self,
            interaction=interaction,
            on_not_found=self._on_slash_not_found,
            default_to_ephemeral=self._defaults_to_ephemeral,
        )
        if self._auto_defer_after is not None:
            ctx.start_defer_timer(self._auto_defer_after)

        hooks = self._get_slash_hooks()
        future = ctx.get_response_future()
        try:
            if await self.check(ctx):
                for component in self._components.values():
                    # This is set on each iteration to ensure that any component
                    # state which was set to this isn't propagated to other components.
                    ctx.set_ephemeral_default(self._defaults_to_ephemeral)
                    if await component.execute_interaction(ctx, hooks=hooks):
                        return await future

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            # Under very specific timing there may be another future which could set a result while we await
            # ctx.respond therefore we create a task to avoid any erroneous behaviour from this trying to create
            # another response before it's returned the initial response.
            asyncio.get_running_loop().create_task(
                ctx.respond(exc.message), name=f"{interaction.id} command error responder"
            )
            return await future

        asyncio.get_running_loop().create_task(ctx.mark_not_found(), name=f"{interaction.id} not found")
        return await future

Tanjun's standard tanjun.abc.Client implementation.

This implementation supports dependency injection for checks, command callbacks, prefix getters and event listeners. For more information on how this works see tanjun.injecting.

Note: By default this client includes a parser error handling hook which will by overwritten if you call Client.set_hooks.

#   Client( rest: hikari.api.rest.RESTClient, *, cache: Optional[hikari.api.cache.Cache] = None, events: Optional[hikari.api.event_manager.EventManager] = None, server: Optional[hikari.api.interaction_server.InteractionServer] = None, shards: Optional[hikari.traits.ShardAware] = None, voice: Optional[hikari.api.voice.VoiceComponent] = None, event_managed: bool = False, mention_prefix: bool = False, set_global_commands: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, bool] = False, declare_global_commands: Union[Sequence[Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int]], hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, bool] = False, command_ids: Optional[collections.abc.Mapping[str, Union[hikari.commands.Command, hikari.snowflakes.Snowflake, int]]] = None, _stack_level: int = 0 )
View Source
    def __init__(
        self,
        rest: hikari.api.RESTClient,
        *,
        cache: typing.Optional[hikari.api.Cache] = None,
        events: typing.Optional[hikari.api.EventManager] = None,
        server: typing.Optional[hikari.api.InteractionServer] = None,
        shards: typing.Optional[hikari_traits.ShardAware] = None,
        voice: typing.Optional[hikari.api.VoiceComponent] = None,
        event_managed: bool = False,
        mention_prefix: bool = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        _stack_level: int = 0,
    ) -> None:
        """Initialise a Tanjun client.

        Notes
        -----
        * For a quicker way to initiate this client around a standard bot aware
        client, see `Client.from_gateway_bot` and `Client.from_rest_bot`.
        * The endpoint used by `declare_global_commands` has a strict ratelimit which,
        as of writing, only allows for 2 requests per minute (with that ratelimit
        either being per-guild if targeting a specific guild otherwise globally).
        * `event_manager` is necessary for message command dispatch and will also
        be necessary for interaction command dispatch if `server` isn't
        provided.
        * `server` is used for interaction command dispatch if interaction
        events aren't being received from the event manager.

        Parameters
        ----------
        rest : hikari.api.rest.RestClient
            The Hikari REST client this will use.

        Other Parameters
        ----------------
        cache : hikari.api.cache.CacheClient
            The Hikari cache client this will use if applicable.
        event_manager : hikari.api.event_manager.EventManagerClient
            The Hikari event manager client this will use if applicable.
        server : hikari.api.interaction_server.InteractionServer
            The Hikari interaction server client this will use if applicable.
        shards : hikari.traits.ShardAware
            The Hikari shard aware client this will use if applicable.
        voice : hikari.api.voice.VoiceComponent
            The Hikari voice component this will use if applicable.
        event_managed : bool
            Whether or not this client is managed by the event manager.

            An event managed client will be automatically started and closed based
            on Hikari's lifetime events.

            Defaults to `False` and can only be passed as `True` if `event_manager`
            is also provided.
        mention_prefix : bool
            Whether or not mention prefixes should be automatically set when this
            client is first started.

            Defaults to `False` and it should be noted that this only applies to
            message commands.
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.

        Raises
        ------
        ValueError
            Raises for the following reasons:
            * If `event_managed` is `True` when `event_manager` is `None`.
            * If `command_ids` is passed when multiple guild ids are provided for `declare_global_commands`.
            * If `command_ids` is passed when `declare_global_commands` is `False`.
        """  # noqa: E501 - line too long
        # InjectorClient.__init__
        super().__init__()
        if _LOGGER.isEnabledFor(logging.INFO):
            _LOGGER.info(
                "%s initialised with the following components: %s",
                "Event-managed client" if event_managed else "Client",
                ", ".join(
                    name
                    for name, value in [
                        ("cache", cache),
                        ("event manager", events),
                        ("interaction server", server),
                        ("rest", rest),
                        ("shard manager", shards),
                    ]
                    if value
                ),
            )

        if not events and not server:
            _LOGGER.warning(
                "Client initiaited without an event manager or interaction server, "
                "automatic command dispatch will be unavailable."
            )

        self._accepts = MessageAcceptsEnum.ALL if events else MessageAcceptsEnum.NONE
        self._auto_defer_after: typing.Optional[float] = 2.0
        self._cache = cache
        self._cached_application_id: typing.Optional[hikari.Snowflake] = None
        self._checks: list[checks.InjectableCheck] = []
        self._client_callbacks: dict[str, list[injecting.CallbackDescriptor[None]]] = {}
        self._components: dict[str, tanjun_abc.Component] = {}
        self._defaults_to_ephemeral: bool = False
        self._make_message_context: _MessageContextMakerProto = context.MessageContext
        self._make_slash_context: _SlashContextMakerProto = context.SlashContext
        self._events = events
        self._grab_mention_prefix = mention_prefix
        self._hooks: typing.Optional[tanjun_abc.AnyHooks] = hooks.AnyHooks().set_on_parser_error(on_parser_error)
        self._interaction_not_found: typing.Optional[str] = "Command not found"
        self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None
        self._is_closing = False
        self._listeners: dict[type[hikari.Event], list[injecting.SelfInjectingCallback[None]]] = {}
        self._loop: typing.Optional[asyncio.AbstractEventLoop] = None
        self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None
        self._metadata: dict[typing.Any, typing.Any] = {}
        self._modules: dict[str, types.ModuleType] = {}
        self._path_modules: dict[pathlib.Path, types.ModuleType] = {}
        self._prefix_getter: typing.Optional[injecting.CallbackDescriptor[collections.Iterable[str]]] = None
        self._prefixes: list[str] = []
        self._rest = rest
        self._server = server
        self._shards = shards
        self._voice = voice

        if event_managed:
            if not events:
                raise ValueError("Client cannot be event managed without an event manager")

            events.subscribe(hikari.StartingEvent, self._on_starting_event)
            events.subscribe(hikari.StoppingEvent, self._on_stopping_event)

        if set_global_commands:
            warnings.warn(
                "The `set_global_commands` argument is deprecated since v2.1.1a1. "
                "Use `declare_global_commands` instead.",
                DeprecationWarning,
                stacklevel=2 + _stack_level,
            )

        declare_global_commands = declare_global_commands or set_global_commands
        command_ids = command_ids or {}
        if isinstance(declare_global_commands, collections.Sequence):
            if command_ids and len(declare_global_commands) > 1:
                raise ValueError(
                    "Cannot provide specific command_ids while automatically "
                    "declaring commands marked as 'global' in multiple-guilds on startup"
                )

            for guild in declare_global_commands:
                _LOGGER.info("Registering startup command declarer for %s guild", guild)
                self.add_client_callback(ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, guild))

        elif isinstance(declare_global_commands, bool):
            if declare_global_commands:
                _LOGGER.info("Registering startup command declarer for global commands")
                if not command_ids:
                    _LOGGER.warning(
                        "No command IDs passed for startup command declarer, this could lead to previously set "
                        "command permissions being lost when commands are renamed."
                    )

                self.add_client_callback(
                    ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, hikari.UNDEFINED)
                )

            elif command_ids:
                raise ValueError("Cannot pass command IDs when not declaring global commands")

        else:
            self.add_client_callback(
                ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, declare_global_commands)
            )

        (
            self.set_type_dependency(tanjun_abc.Client, self)
            .set_type_dependency(Client, self)
            .set_type_dependency(type(self), self)
            .set_type_dependency(hikari.api.RESTClient, rest)
            .set_type_dependency(type(rest), rest)
        )
        if cache:
            self.set_type_dependency(hikari.api.Cache, cache).set_type_dependency(type(cache), cache)

        if events:
            self.set_type_dependency(hikari.api.EventManager, events).set_type_dependency(type(events), events)

        if server:
            self.set_type_dependency(hikari.api.InteractionServer, server).set_type_dependency(type(server), server)

        if shards:
            self.set_type_dependency(hikari_traits.ShardAware, shards).set_type_dependency(type(shards), shards)

        if voice:
            self.set_type_dependency(hikari.api.VoiceComponent, voice).set_type_dependency(type(voice), voice)

        dependencies.set_standard_dependencies(self)

Initialise a Tanjun client.

Notes
  • For a quicker way to initiate this client around a standard bot aware client, see Client.from_gateway_bot and Client.from_rest_bot.
  • The endpoint used by declare_global_commands has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
  • event_manager is necessary for message command dispatch and will also be necessary for interaction command dispatch if server isn't provided.
  • server is used for interaction command dispatch if interaction events aren't being received from the event manager.
Parameters
  • rest (hikari.api.rest.RestClient): The Hikari REST client this will use.
Other Parameters
  • cache (hikari.api.cache.CacheClient): The Hikari cache client this will use if applicable.
  • event_manager (hikari.api.event_manager.EventManagerClient): The Hikari event manager client this will use if applicable.
  • server (hikari.api.interaction_server.InteractionServer): The Hikari interaction server client this will use if applicable.
  • shards (hikari.traits.ShardAware): The Hikari shard aware client this will use if applicable.
  • voice (hikari.api.voice.VoiceComponent): The Hikari voice component this will use if applicable.
  • event_managed (bool): Whether or not this client is managed by the event manager.

    An event managed client will be automatically started and closed based on Hikari's lifetime events.

    Defaults to False and can only be passed as True if event_manager is also provided.

  • mention_prefix (bool): Whether or not mention prefixes should be automatically set when this client is first started.

    Defaults to False and it should be noted that this only applies to message commands.

  • declare_global_commands (typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Whether or not to automatically set global slash commands when this client is first started. Defaults to False.

    If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.

  • set_global_commands (typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Deprecated as of v2.1.1a1 alias of declare_global_commands.
  • command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the commands to update.

    This field is complementary to declare_global_commands and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename.

    This currently isn't supported when multiple guild IDs are passed for declare_global_commands.

Raises
  • ValueError: Raises for the following reasons:
#  
@classmethod
def from_gateway_bot( cls, bot: hikari.traits.GatewayBotAware, /, *, event_managed: bool = True, mention_prefix: bool = False, declare_global_commands: Union[Sequence[Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int]], hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, bool] = False, set_global_commands: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, bool] = False, command_ids: Optional[collections.abc.Mapping[str, Union[hikari.commands.Command, hikari.snowflakes.Snowflake, int]]] = None ) -> tanjun.clients.Client:
View Source
    @classmethod
    def from_gateway_bot(
        cls,
        bot: hikari_traits.GatewayBotAware,
        /,
        *,
        event_managed: bool = True,
        mention_prefix: bool = False,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
    ) -> Client:
        """Build a `Client` from a `hikari.traits.GatewayBotAware` instance.

        Notes
        -----
        * This implicitly defaults the client to human only mode.
        * This sets type dependency injectors for the hikari traits present in
          `bot` (including `hikari.traits.GatewayBotAware`).
        * The endpoint used by `declare_global_commands` has a strict ratelimit
          which, as of writing, only allows for 2 requests per minute (with that
          ratelimit either being per-guild if targeting a specific guild
          otherwise globally).

        Parameters
        ----------
        bot : hikari.traits.GatewayBotAware
            The bot client to build from.

            This will be used to infer the relevant Hikari clients to use.

        Other Parameters
        ----------------
        event_managed : bool
            Whether or not this client is managed by the event manager.

            An event managed client will be automatically started and closed
            based on Hikari's lifetime events.

            Defaults to `True`.
        mention_prefix : bool
            Whether or not mention prefixes should be automatically set when this
            client is first started.

            Defaults to `False` and it should be noted that this only applies to
            message commands.
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.
        """  # noqa: E501 - line too long
        return (
            cls(
                rest=bot.rest,
                cache=bot.cache,
                events=bot.event_manager,
                shards=bot,
                voice=bot.voice,
                event_managed=event_managed,
                mention_prefix=mention_prefix,
                declare_global_commands=declare_global_commands,
                set_global_commands=set_global_commands,
                command_ids=command_ids,
                _stack_level=1,
            )
            .set_human_only()
            .set_hikari_trait_injectors(bot)
        )

Build a Client from a hikari.traits.GatewayBotAware instance.

Notes
  • This implicitly defaults the client to human only mode.
  • This sets type dependency injectors for the hikari traits present in bot (including hikari.traits.GatewayBotAware).
  • The endpoint used by declare_global_commands has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
Parameters
  • bot (hikari.traits.GatewayBotAware): The bot client to build from.

    This will be used to infer the relevant Hikari clients to use.

Other Parameters
  • event_managed (bool): Whether or not this client is managed by the event manager.

    An event managed client will be automatically started and closed based on Hikari's lifetime events.

    Defaults to True.

  • mention_prefix (bool): Whether or not mention prefixes should be automatically set when this client is first started.

    Defaults to False and it should be noted that this only applies to message commands.

  • declare_global_commands (typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Whether or not to automatically set global slash commands when this client is first started. Defaults to False.

    If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.

  • set_global_commands (typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Deprecated as of v2.1.1a1 alias of declare_global_commands.
  • command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the commands to update.

    This field is complementary to declare_global_commands and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename.

    This currently isn't supported when multiple guild IDs are passed for declare_global_commands.

#  
@classmethod
def from_rest_bot( cls, bot: hikari.traits.RESTBotAware, /, *, declare_global_commands: Union[Sequence[Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int]], hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, bool] = False, set_global_commands: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, bool] = False, command_ids: Optional[collections.abc.Mapping[str, Union[hikari.commands.Command, hikari.snowflakes.Snowflake, int]]] = None ) -> tanjun.clients.Client:
View Source
    @classmethod
    def from_rest_bot(
        cls,
        bot: hikari_traits.RESTBotAware,
        /,
        *,
        declare_global_commands: typing.Union[
            hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool
        ] = False,
        set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
    ) -> Client:
        """Build a `Client` from a `hikari.traits.RESTBotAware` instance.

        Notes
        -----
        * This sets type dependency injectors for the hikari traits present in
          `bot` (including `hikari.traits.RESTBotAware`).
        * The endpoint used by `declare_global_commands` has a strict ratelimit
          which, as of writing, only allows for 2 requests per minute (with that
          ratelimit either being per-guild if targeting a specific guild
          otherwise globally).

        Parameters
        ----------
        bot : hikari.traits.RESTBotAware
            The bot client to build from.

        Other Parameters
        ----------------
        declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Whether or not to automatically set global slash commands when this
            client is first started. Defaults to `False`.

            If one or more guild objects/IDs are passed here then the registered
            global commands will be set on the specified guild(s) at startup rather
            than globally. This can be useful for testing/debug purposes as slash
            commands may take up to an hour to propagate globally but will
            immediately propagate when set on a specific guild.
        set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]
            Deprecated as of v2.1.1a1 alias of `declare_global_commands`.
        command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]
            If provided, a mapping of top level command names to IDs of the commands to update.

            This field is complementary to `declare_global_commands` and, while it
            isn't necessarily required, this will in some situations help avoid
            permissions which were previously set for a command from being lost
            after a rename.

            This currently isn't supported when multiple guild IDs are passed for
            `declare_global_commands`.
        """  # noqa: E501 - line too long
        return cls(
            rest=bot.rest,
            server=bot.interaction_server,
            declare_global_commands=declare_global_commands,
            set_global_commands=set_global_commands,
            command_ids=command_ids,
            _stack_level=1,
        ).set_hikari_trait_injectors(bot)

Build a Client from a hikari.traits.RESTBotAware instance.

Notes
  • This sets type dependency injectors for the hikari traits present in bot (including hikari.traits.RESTBotAware).
  • The endpoint used by declare_global_commands has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
Parameters
  • bot (hikari.traits.RESTBotAware): The bot client to build from.
Other Parameters
  • declare_global_commands (typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Whether or not to automatically set global slash commands when this client is first started. Defaults to False.

    If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.

  • set_global_commands (typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Deprecated as of v2.1.1a1 alias of declare_global_commands.
  • command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the commands to update.

    This field is complementary to declare_global_commands and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename.

    This currently isn't supported when multiple guild IDs are passed for declare_global_commands.

#   defaults_to_ephemeral: bool

Whether slash contexts spawned by this client should default to ephemeral responses.

This effects calls to SlashContext.create_followup, SlashContext.create_initial_response, SlashContext.defer and SlashContext.respond unless the flags field is provided for the methods which support it.

Notes
  • This may be overridden by BaseSlashCommand.defaults_to_ephemeral and Component.defaults_to_ephemeral.
  • This defaults to False.
  • This only effects slash command execution.

Type of message create events this command client accepts for execution.

#   is_human_only: bool

Whether this client is only executing for non-bot/webhook users messages.

#   cache: Optional[hikari.api.cache.Cache]

Hikari cache instance this command client was initialised with.

#   checks: collections.abc.Collection[collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]]]

Collection of the level tanjun.abc.Context checks registered to this client.

Note: These may be taking advantage of the standard dependency injection.

#   components: collections.abc.Collection[tanjun.abc.Component]

Collection of the components this command client is using.

#   events: Optional[hikari.api.event_manager.EventManager]

Object of the event manager this client was initialised with.

This is used for executing message commands if set.

#   listeners: collections.abc.Mapping[type[hikari.events.base_events.Event], collections.abc.Collection[collections.abc.Callable[..., collections.abc.Coroutine[typing.Any, typing.Any, None]]]]

Mapping of event types to the listeners registered in this client.

Top level tanjun.abc.AnyHooks set for this client.

These are called during both message and interaction command execution.

Returns

Top level tanjun.abc.SlashHooks set for this client.

These are only called during interaction command execution.

#   is_alive: bool

Whether this client is alive.

#   loop: Optional[asyncio.events.AbstractEventLoop]

The loop this client is bound to if it's alive.

Top level tanjun.abc.MessageHooks set for this client.

These are only called during both message command execution.

#   metadata: collections.abc.MutableMapping[typing.Any, typing.Any]

Mutable mapping of the metadata set for this client.

Note: Any modifications made to this mutable mapping will be preserved by the client.

#   prefix_getter: Optional[collections.abc.Callable[..., collections.abc.Awaitable[collections.abc.Iterable[str]]]]

Prefix getter method set for this client.

For more information on this callback's signature see PrefixGetter.

#   prefixes: collections.abc.Collection[str]

Collection of the standard prefixes set for this client.

#   rest: hikari.api.rest.RESTClient

Object of the Hikari REST client this client was initialised with.

#   server: Optional[hikari.api.interaction_server.InteractionServer]

Object of the Hikari interaction server provided for this client.

This is used for executing slash commands if set.

#   shards: Optional[hikari.traits.ShardAware]

Object of the Hikari shard manager this client was initialised with.

#   voice: Optional[hikari.api.voice.VoiceComponent]

Object of the Hikari voice component this client was initialised with.

#   async def clear_application_commands( self, *, application: Union[hikari.guilds.PartialApplication, hikari.snowflakes.Snowflake, int, NoneType] = None, guild: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, hikari.undefined.UndefinedType] = UNDEFINED ) -> None:
View Source
    async def clear_application_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        if application is None:
            application = self._cached_application_id or await self.fetch_rest_application_id()

        await self._rest.set_application_commands(application, (), guild=guild)

Clear the commands declared either globally or for a specific guild.

Note: The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).

Other Parameters
  • application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): The application to clear commands for.

    If left as None then this will be inferred from the authorization being used by Client.rest.

  • guild (hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to clear commands for.

    If left as None global commands will be cleared.

#   async def set_global_commands( self, *, application: Union[hikari.guilds.PartialApplication, hikari.snowflakes.Snowflake, int, NoneType] = None, guild: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, hikari.undefined.UndefinedType] = UNDEFINED, force: bool = False ) -> collections.abc.Sequence[hikari.commands.Command]:
View Source
    async def set_global_commands(
        self,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        """Alias of `Client.declare_global_commands`.

        .. deprecated:: v2.1.1a1
            Use `Client.declare_global_commands` instead.
        """
        warnings.warn(
            "The `Client.set_global_commands` method has been deprecated since v2.1.1a1. "
            "Use `Client.declare_global_commands` instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return await self.declare_global_commands(application=application, guild=guild, force=force)

Alias of Client.declare_global_commands.

Deprecated since version v2.1.1a1: Use Client.declare_global_commands instead.

#   async def declare_global_commands( self, command_ids: Optional[collections.abc.Mapping[str, Union[hikari.commands.Command, hikari.snowflakes.Snowflake, int]]] = None, *, application: Union[hikari.guilds.PartialApplication, hikari.snowflakes.Snowflake, int, NoneType] = None, guild: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, hikari.undefined.UndefinedType] = UNDEFINED, force: bool = False ) -> collections.abc.Sequence[hikari.commands.Command]:
View Source
    async def declare_global_commands(
        self,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        # <<inherited docstring from tanjun.abc.Client>>.
        commands = (
            command
            for command in itertools.chain.from_iterable(
                component.slash_commands for component in self._components.values()
            )
            if command.is_global
        )
        return await self.declare_application_commands(
            commands, command_ids, application=application, guild=guild, force=force
        )

Set the global application commands for a bot based on the loaded components.

Warning: This will overwrite any previously set application commands and only targets commands marked as global.

Notes
  • The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
  • Setting a specific guild can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.
Other Parameters
  • command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the existing commands to update.
  • application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): Object or ID of the application to set the global commands for.

    If left as None then this will be inferred from the authorization being used by Client.rest.

  • guild (hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to set the global commands to.

    If left as None global commands will be set.

  • force (bool): Force this to declare the commands regardless of whether or not they match the current state of the declared commands.

    Defaults to False. This default behaviour helps avoid issues with the 2 request per minute (per-guild or globally) ratelimit and the other limit of only 200 application command creates per day (per guild or globally).

Returns
  • collections.abc.Sequence[hikari..Command]: API representations of the set commands.
#   async def declare_application_command( self, command: tanjun.abc.BaseSlashCommand, /, command_id: Union[hikari.snowflakes.Snowflake, int, NoneType] = None, *, application: Union[hikari.guilds.PartialApplication, hikari.snowflakes.Snowflake, int, NoneType] = None, guild: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, hikari.undefined.UndefinedType] = UNDEFINED ) -> hikari.commands.Command:
View Source
    async def declare_application_command(
        self,
        command: tanjun_abc.BaseSlashCommand,
        /,
        command_id: typing.Optional[hikari.Snowflakeish] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
    ) -> hikari.Command:
        # <<inherited docstring from tanjun.abc.Client>>.
        builder = command.build()
        if command_id:
            response = await self._rest.edit_application_command(
                application or self._cached_application_id or await self.fetch_rest_application_id(),
                command_id,
                guild=guild,
                name=builder.name,
                description=builder.description,
                options=builder.options,
            )

        else:
            response = await self._rest.create_application_command(
                application or self._cached_application_id or await self.fetch_rest_application_id(),
                guild=guild,
                name=builder.name,
                description=builder.description,
                options=builder.options,
            )

        if not guild:
            command.set_tracked_command(response)  # TODO: is this fine?

        return response

Declare a single slash command for a bot.

Warning: Providing command_id when updating a command helps avoid any permissions set for the command being lose (e.g. when changing the command's name).

Parameters
  • command (BaseSlashCommand): The command to register.
Other Parameters
  • application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): The application to register the command with.

    If left as None then this will be inferred from the authorization being used by Client.rest.

  • command_id (typing.Optional[hikari.snowflakes.Snowflakeish]): ID of the command to update.
  • guild (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to register the command with.

    If left as None then the command will be registered globally.

Returns
  • hikari.Command: API representation of the command that was registered.
#   async def declare_application_commands( self, commands: collections.abc.Iterable[tanjun.abc.BaseSlashCommand], /, command_ids: Optional[collections.abc.Mapping[str, Union[hikari.commands.Command, hikari.snowflakes.Snowflake, int]]] = None, *, application: Union[hikari.guilds.PartialApplication, hikari.snowflakes.Snowflake, int, NoneType] = None, guild: Union[hikari.guilds.PartialGuild, hikari.snowflakes.Snowflake, int, hikari.undefined.UndefinedType] = UNDEFINED, force: bool = False ) -> collections.abc.Sequence[hikari.commands.Command]:
View Source
    async def declare_application_commands(
        self,
        commands: collections.Iterable[tanjun_abc.BaseSlashCommand],
        /,
        command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None,
        *,
        application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None,
        guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED,
        force: bool = False,
    ) -> collections.Sequence[hikari.Command]:
        # <<inherited docstring from tanjun.abc.Client>>.
        command_ids = command_ids or {}
        names_to_commands: dict[str, tanjun_abc.BaseSlashCommand] = {}
        conflicts: set[str] = set()
        builders: dict[str, hikari.api.CommandBuilder] = {}

        for command in commands:
            names_to_commands[command.name] = command
            if command.name in builders:
                conflicts.add(command.name)

            builder = command.build()
            if command_id := command_ids.get(command.name):
                builder.set_id(hikari.Snowflake(command_id))

            builders[command.name] = builder

        if conflicts:
            raise ValueError(
                "Couldn't declare commands due to conflicts. The following command names have more than one command "
                "registered for them " + ", ".join(conflicts)
            )

        if len(builders) > 100:
            raise ValueError("You can only declare up to 100 top level commands in a guild or globally")

        if not application:
            application = self._cached_application_id or await self.fetch_rest_application_id()

        target_type = "global" if guild is hikari.UNDEFINED else f"guild {int(guild)}"

        if not force:
            registered_commands = await self._rest.fetch_application_commands(application, guild=guild)
            if len(registered_commands) == len(builders) and all(
                _cmp_command(builders.get(command.name), command) for command in registered_commands
            ):
                _LOGGER.info("Skipping bulk declare for %s slash commands since they're already declared", target_type)
                return registered_commands

        _LOGGER.info("Bulk declaring %s %s slash commands", len(builders), target_type)
        responses = await self._rest.set_application_commands(application, list(builders.values()), guild=guild)

        for response in responses:
            if not guild:
                names_to_commands[response.name].set_tracked_command(response)  # TODO: is this fine?

            if (expected_id := command_ids.get(response.name)) and hikari.Snowflake(expected_id) != response.id:
                _LOGGER.warning(
                    "ID mismatch found for %s command %r, expected %s but got %s. "
                    "This suggests that any previous permissions set for this command will have been lost.",
                    target_type,
                    response.name,
                    expected_id,
                    response.id,
                )

        _LOGGER.info("Successfully declared %s (top-level) %s commands", len(responses), target_type)
        if _LOGGER.isEnabledFor(logging.DEBUG):
            _LOGGER.debug(
                "Declared %s command ids; %s",
                target_type,
                ", ".join(f"{response.name}: {response.id}" for response in responses),
            )

        return responses

Declare a collection of slash commands for a bot.

Note: The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).

Parameters
  • commands (collections.abc.Iterable[BaseSlashCommand]): Iterable of the commands to register.
Other Parameters
  • command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the existing commands to update.

    While optional, this can be helpful when updating commands as providing the current IDs will prevent changes such as renames from leading to other state set for commands (e.g. permissions) from being lost.

  • application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): The application to register the commands with.

    If left as None then this will be inferred from the authorization being used by Client.rest.

  • guild (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to register the commands with.

    If left as None then the commands will be registered globally.

  • force (bool): Force this to declare the commands regardless of whether or not they match the current state of the declared commands.

    Defaults to False. This default behaviour helps avoid issues with the 2 request per minute (per-guild or globally) ratelimit and the other limit of only 200 application command creates per day (per guild or globally).

Returns
  • collections.abc.Sequence[hikari.Command]: API representations of the commands which were registered.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If conflicting command names are found (multiple commanbds have the same top-level name).
    • If more than 100 top-level commands are passed.
#   def set_auto_defer_after(self: ~_ClientT, time: Optional[float], /) -> ~_ClientT:
View Source
    def set_auto_defer_after(self: _ClientT, time: typing.Optional[float], /) -> _ClientT:
        """Set when this client should automatically defer execution of commands.

        .. warning::
            If `time` is set to `None` then automatic deferrals will be disabled.
            This may lead to unexpected behaviour.

        Parameters
        ----------
        time : typing.Optional[float]
            The time in seconds to defer interaction command responses after.
        """
        self._auto_defer_after = float(time) if time is not None else None
        return self

Set when this client should automatically defer execution of commands.

Warning: If time is set to None then automatic deferrals will be disabled. This may lead to unexpected behaviour.

Parameters
  • time (typing.Optional[float]): The time in seconds to defer interaction command responses after.
#   def set_ephemeral_default(self: ~_ClientT, state: bool, /) -> ~_ClientT:
View Source
    def set_ephemeral_default(self: _ClientT, state: bool, /) -> _ClientT:
        """Set whether slash contexts spawned by this client should default to ephemeral responses.

        Parameters
        ----------
        bool
            Whether slash command contexts executed in this component should
            should default to ephemeral.

            This will be overridden by any response calls which specify flags
            and defaults to `False`.

        Returns
        -------
        SelfT
            This component to enable method chaining.
        """
        self._defaults_to_ephemeral = state
        return self

Set whether slash contexts spawned by this client should default to ephemeral responses.

Parameters
  • bool: Whether slash command contexts executed in this component should should default to ephemeral.

This will be overridden by any response calls which specify flags and defaults to False.

Returns
  • SelfT: This component to enable method chaining.
#   def set_hikari_trait_injectors(self: ~_ClientT, bot: hikari.traits.RESTAware, /) -> ~_ClientT:
View Source
    def set_hikari_trait_injectors(self: _ClientT, bot: hikari_traits.RESTAware, /) -> _ClientT:
        """Set type based dependency injection based on the hikari traits found in `bot`.

        This is a short hand for calling `Client.add_type_dependency` for all
        the hikari trait types `bot` is valid for with bot.

        Parameters
        ----------
        bot : hikari_traits.RESTAware
            The hikari client to set dependency injectors for.
        """
        for _, member in inspect.getmembers(hikari_traits):
            if inspect.isclass(member) and isinstance(bot, member):
                self.set_type_dependency(member, bot)

        return self

Set type based dependency injection based on the hikari traits found in bot.

This is a short hand for calling Client.add_type_dependency for all the hikari trait types bot is valid for with bot.

Parameters
  • bot (hikari_traits.RESTAware): The hikari client to set dependency injectors for.
#   def set_interaction_not_found(self: ~_ClientT, message: Optional[str], /) -> ~_ClientT:
View Source
    def set_interaction_not_found(self: _ClientT, message: typing.Optional[str], /) -> _ClientT:
        """Set the response message for when an interaction command is not found.

        .. warning::
            Setting this to `None` may lead to unexpected behaviour (especially
            when the client is still set to auto-defer interactions) and should
            only be done if you know what you're doing.

        Parameters
        ----------
        message : typing.Optional[str]
            The message to respond with when an interaction command isn't found.
        """
        self._interaction_not_found = message
        return self

Set the response message for when an interaction command is not found.

Warning: Setting this to None may lead to unexpected behaviour (especially when the client is still set to auto-defer interactions) and should only be done if you know what you're doing.

Parameters
  • message (typing.Optional[str]): The message to respond with when an interaction command isn't found.
#   def set_message_accepts( self: ~_ClientT, accepts: tanjun.clients.MessageAcceptsEnum, / ) -> ~_ClientT:
View Source
    def set_message_accepts(self: _ClientT, accepts: MessageAcceptsEnum, /) -> _ClientT:
        """Set the kind of messages commands should be executed based on.

        Parameters
        ----------
        accepts : MessageAcceptsEnum
            The type of messages commands should be executed based on.
        """
        if accepts.get_event_type() and not self._events:
            raise ValueError("Cannot set accepts level on a client with no event manager")

        self._accepts = accepts
        return self

Set the kind of messages commands should be executed based on.

Parameters
  • accepts (MessageAcceptsEnum): The type of messages commands should be executed based on.
#   def set_message_ctx_maker( self: ~_ClientT, maker: tanjun.clients._MessageContextMakerProto = <class 'tanjun.context.MessageContext'>, / ) -> ~_ClientT:
View Source
    def set_message_ctx_maker(self: _ClientT, maker: _MessageContextMakerProto = context.MessageContext, /) -> _ClientT:
        """Set the message context maker to use when creating context for a message.

        .. warning::
            The caller must return an instance of `tanjun.context.MessageContext`
            rather than just any implementation of the MessageContext abc due to
            this client relying on implementation detail of
            `tanjun.context.MessageContext`.

        Parameters
        ----------
        maker : _MessageContextMakerProto
            The message context maker to use.

            This is a callback which should match the signature of
            `tanjun.context.MessageContext.__init__` and return an instance
            of `tanjun.context.MessageContext`.

            This defaults to `tanjun.context.MessageContext`.
        """
        self._make_message_context = maker
        return self

Set the message context maker to use when creating context for a message.

Warning: The caller must return an instance of tanjun.context.MessageContext rather than just any implementation of the MessageContext abc due to this client relying on implementation detail of tanjun.context.MessageContext.

Parameters
#   def set_metadata(self: ~_ClientT, key: Any, value: Any, /) -> ~_ClientT:
View Source
    def set_metadata(self: _ClientT, key: typing.Any, value: typing.Any, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        self._metadata[key] = value
        return self

Set a field in the client's metadata.

Parameters
  • key (typing.Any): Metadata key to set.
  • value (typing.Any): Metadata value to set.
Returns
  • Self: The client instance to enable chained calls.
#   def set_slash_ctx_maker( self: ~_ClientT, maker: tanjun.clients._SlashContextMakerProto = <class 'tanjun.context.SlashContext'>, / ) -> ~_ClientT:
View Source
    def set_slash_ctx_maker(self: _ClientT, maker: _SlashContextMakerProto = context.SlashContext, /) -> _ClientT:
        """Set the slash context maker to use when creating context for a slash command.

        .. warning::
            The caller must return an instance of `tanjun.context.SlashContext`
            rather than just any implementation of the SlashContext abc due to
            this client relying on implementation detail of
            `tanjun.context.SlashContext`.

        Parameters
        ----------
        maker : _SlashContextMakerProto
            The slash context maker to use.

            This is a callback which should match the signature of
            `tanjun.context.SlashContext.__init__` and return an instance
            of `tanjun.context.SlashContext`.

            This defaults to `tanjun.context.SlashContext`.
        """
        self._make_slash_context = maker
        return self

Set the slash context maker to use when creating context for a slash command.

Warning: The caller must return an instance of tanjun.context.SlashContext rather than just any implementation of the SlashContext abc due to this client relying on implementation detail of tanjun.context.SlashContext.

Parameters
#   def set_human_only(self: ~_ClientT, value: bool = True) -> ~_ClientT:
View Source
    def set_human_only(self: _ClientT, value: bool = True) -> _ClientT:
        """Set whether or not message commands execution should be limited to "human" users.

        .. note::
            This doesn't apply to interaction commands as these can only be
            triggered by a "human" (normal user account).

        Parameters
        ----------
        value : bool
            Whether or not message commands execution should be limited to "human" users.

            Passing `True` here will prevent message commands from being executed
            based on webhook and bot messages.
        """
        if value:
            self.add_check(_check_human)

        else:
            try:
                self.remove_check(_check_human)
            except ValueError:
                pass

        return self

Set whether or not message commands execution should be limited to "human" users.

Note: This doesn't apply to interaction commands as these can only be triggered by a "human" (normal user account).

Parameters
  • value (bool): Whether or not message commands execution should be limited to "human" users.

    Passing True here will prevent message commands from being executed based on webhook and bot messages.

#   def add_check( self: ~_ClientT, check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], / ) -> ~_ClientT:
View Source
    def add_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT:
        """Add a generic check to this client.

        This will be applied to both message and slash command execution.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to add. This may be either synchronous or asynchronous
            and must take one positional argument of type `tanjun.abc.Context`
            with dependency injection being supported for its keyword arguments.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        if check not in self._checks:
            self._checks.append(checks.InjectableCheck(check))

        return self

Add a generic check to this client.

This will be applied to both message and slash command execution.

Parameters
  • check (tanjun_abc.CheckSig): The check to add. This may be either synchronous or asynchronous and must take one positional argument of type tanjun.abc.Context with dependency injection being supported for its keyword arguments.
Returns
  • Self: The client instance to enable chained calls.
#   def remove_check( self: ~_ClientT, check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], / ) -> ~_ClientT:
View Source
    def remove_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT:
        """Remove a check from the client.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to remove.

        Raises
        ------
        ValueError
            If the check was not previously added.
        """
        self._checks.remove(typing.cast("checks.InjectableCheck", check))
        return self

Remove a check from the client.

Parameters
  • check (tanjun_abc.CheckSig): The check to remove.
Raises
  • ValueError: If the check was not previously added.
#   def with_check(self, check: ~CheckSigT, /) -> ~CheckSigT:
View Source
    def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT:
        """Add a check to this client through a decorator call.

        Parameters
        ----------
        check : tanjun_abc.CheckSig
            The check to add. This may be either synchronous or asynchronous
            and must take one positional argument of type `tanjun.abc.Context`
            with dependency injection being supported for its keyword arguments.

        Returns
        -------
        tanjun_abc.CheckSig
            The added check.
        """
        self.add_check(check)
        return check

Add a check to this client through a decorator call.

Parameters
  • check (tanjun_abc.CheckSig): The check to add. This may be either synchronous or asynchronous and must take one positional argument of type tanjun.abc.Context with dependency injection being supported for its keyword arguments.
Returns
  • tanjun_abc.CheckSig: The added check.
#   async def check(self, ctx: tanjun.abc.Context, /) -> bool:
View Source
    async def check(self, ctx: tanjun_abc.Context, /) -> bool:
        return await utilities.gather_checks(ctx, self._checks)
#   def add_component( self: ~_ClientT, component: tanjun.abc.Component, /, *, add_injector: bool = False ) -> ~_ClientT:
View Source
    def add_component(self: _ClientT, component: tanjun_abc.Component, /, *, add_injector: bool = False) -> _ClientT:
        """Add a component to this client.

        Parameters
        ----------
        component: Component
            The component to move to this client.

        Returns
        -------
        Self
            The client instance to allow chained calls.

        Raises
        ------
        ValueError
            If the component's name is already registered.
        """
        if component.name in self._components:
            raise ValueError(f"A component named {component.name!r} is already registered.")

        component.bind_client(self)
        self._components[component.name] = component

        if add_injector:
            self.set_type_dependency(type(component), lambda: component)

        if self._loop:
            self._loop.create_task(component.open())
            self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.COMPONENT_ADDED, component))

        return self

Add a component to this client.

Parameters
  • component (Component): The component to move to this client.
Returns
  • Self: The client instance to allow chained calls.
Raises
  • ValueError: If the component's name is already registered.
#   def get_component_by_name(self, name: str, /) -> Optional[tanjun.abc.Component]:
View Source
    def get_component_by_name(self, name: str, /) -> typing.Optional[tanjun_abc.Component]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self._components.get(name)

Get a component from this client by name.

Parameters
  • name (str): Name to get a component by.
Returns
  • typing.Optional[Component]: The component instance if found, else None.
#   def remove_component(self: ~_ClientT, component: tanjun.abc.Component, /) -> ~_ClientT:
View Source
    def remove_component(self: _ClientT, component: tanjun_abc.Component, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        stored_component = self._components.get(component.name)
        if not stored_component or stored_component != component:
            raise ValueError(f"The component {component!r} is not registered.")

        del self._components[component.name]

        if self._loop:
            self._loop.create_task(component.close(unbind=True))
            self._loop.create_task(
                self.dispatch_client_callback(ClientCallbackNames.COMPONENT_REMOVED, stored_component)
            )

        else:
            stored_component.unbind_client(self)

        return self

Remove a component from this client.

This will unsubscribe any client callbacks, commands and listeners registered in the provided component.

Parameters
  • component (Component): The component to remove from this client.
Raises
  • ValueError: If the provided component isn't found.
Returns
  • Self: The client instance to allow chained calls.
#   def remove_component_by_name(self: ~_ClientT, name: str, /) -> ~_ClientT:
View Source
    def remove_component_by_name(self: _ClientT, name: str, /) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        return self.remove_component(self._components[name])

Remove a component from this client by name.

This will unsubscribe any client callbacks, commands and listeners registered in the provided component.

Parameters
  • name (str): Name of the component to remove from this client.
Raises
  • KeyError: If the provided component name isn't found.
#   def add_client_callback( self: ~_ClientT, name: Union[str, tanjun.abc.ClientCallbackNames], callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_ClientT:
View Source
    def add_client_callback(
        self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        descriptor = injecting.CallbackDescriptor(callback)
        name = name.casefold()
        try:
            if descriptor in self._client_callbacks[name]:
                return self

            self._client_callbacks[name].append(descriptor)
        except KeyError:
            self._client_callbacks[name] = [descriptor]

        return self

Add a client callback.

Parameters
  • name (typing.Union[str, ClientCallbackNames]): The name this callback is being registered to.

    This is case-insensitive.

  • callback (MetaEventSigT): The callback to register.

    This may be sync or async and must return None. The positional and keyword arguments a callback should expect depend on implementation detail around the name being subscribed to.

Returns
  • Self: The client instance to enable chained calls.
#   async def dispatch_client_callback( self, name: Union[str, tanjun.abc.ClientCallbackNames], /, *args: Any ) -> None:
View Source
    async def dispatch_client_callback(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /, *args: typing.Any
    ) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        if callbacks := self._client_callbacks.get(name):
            calls = (
                _wrap_client_callback(callback, injecting.BasicInjectionContext(self), args) for callback in callbacks
            )
            await asyncio.gather(*calls)

Dispatch a client callback.

Parameters
  • name (typing.Union[str, ClientCallbackNames]): The name of the callback to dispatch.
Other Parameters
  • *args (typing.Any): Positional arguments to pass to the callback(s).
Raises
  • KeyError: If no callbacks are registered for the given name.
#   def get_client_callbacks( self, name: Union[str, tanjun.abc.ClientCallbackNames], / ) -> collections.abc.Collection[collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]]]:
View Source
    def get_client_callbacks(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Collection[tanjun_abc.MetaEventSig]:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        if result := self._client_callbacks.get(name):
            return tuple(callback.callback for callback in result)

        return ()

Get a collection of the callbacks registered for a specific name.

Parameters
  • name (typing.Union[str, ClientCallbackNames]): The name to get the callbacks registered for.

    This is case-insensitive.

Returns
  • collections.abc.Collection[MetaEventSig]: Collection of the callbacks for the provided name.
#   def remove_client_callback( self: ~_ClientT, name: Union[str, tanjun.abc.ClientCallbackNames], callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_ClientT:
View Source
    def remove_client_callback(
        self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        name = name.casefold()
        self._client_callbacks[name].remove(typing.cast("injecting.CallbackDescriptor[None]", callback))
        if not self._client_callbacks[name]:
            del self._client_callbacks[name]

        return self

Remove a client callback.

Parameters
  • name (typing.Union[str, ClientCallbackNames]): The name this callback is being registered to.

    This is case-insensitive.

  • callback (MetaEventSigT): The callback to remove from the client's callbacks.
Raises
  • KeyError: If the provided name isn't found.
  • ValueError: If the provided callback isn't found.
Returns
  • Self: The client instance to enable chained calls.
#   def with_client_callback( self, name: Union[str, tanjun.abc.ClientCallbackNames], / ) -> collections.abc.Callable[[~MetaEventSigT], ~MetaEventSigT]:
View Source
    def with_client_callback(
        self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]:
        # <<inherited docstring from tanjun.abc.Client>>.
        def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT:
            self.add_client_callback(name, callback)
            return callback

        return decorator

Add a client callback through a decorator call.

Examples
client = tanjun.Client.from_rest_bot(bot)

@client.with_client_callback("closed")
async def on_close() -> None:
    raise NotImplementedError
Parameters
  • name (typing.Union[str, ClientCallbackNames]): The name this callback is being registered to.

    This is case-insensitive.

Returns
  • collections.abc.Callable[[MetaEventSigT], MetaEventSigT]: Decorator callback used to register the client callback.

This may be sync or async and must return None. The positional and keyword arguments a callback should expect depend on implementation detail around the name being subscribed to.

#   def add_listener( self: ~_ClientT, event_type: type[hikari.events.base_events.Event], callback: collections.abc.Callable[..., collections.abc.Coroutine[typing.Any, typing.Any, None]], / ) -> ~_ClientT:
View Source
    def add_listener(
        self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        injected: injecting.SelfInjectingCallback[None] = injecting.SelfInjectingCallback(self, callback)
        try:
            if callback in self._listeners[event_type]:
                return self

            self._listeners[event_type].append(injected)

        except KeyError:
            self._listeners[event_type] = [injected]

        if self._loop and self._events:
            self._events.subscribe(event_type, injected.__call__)

        return self

Add a listener to the client.

Parameters
  • event_type (type[hikari.Event]): The event type to add a listener for.
  • callback (ListenerCallbackSig): The callback to register as a listener.

    This callback must be a coroutine function which returns None and always takes at least one positional arg of type hikari.Event regardless of client implementation detail.

Returns
  • Self: The client instance to enable chained calls.
#   def remove_listener( self: ~_ClientT, event_type: type[hikari.events.base_events.Event], callback: collections.abc.Callable[..., collections.abc.Coroutine[typing.Any, typing.Any, None]], / ) -> ~_ClientT:
View Source
    def remove_listener(
        self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, /
    ) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        index = self._listeners[event_type].index(typing.cast("injecting.SelfInjectingCallback[None]", callback))
        registered_callback = self._listeners[event_type].pop(index)

        if not self._listeners[event_type]:
            del self._listeners[event_type]

        if self._loop and self._events:
            self._events.unsubscribe(event_type, registered_callback.__call__)

        return self

Remove a listener from the client.

Parameters
  • event_type (type[hikari.Event]): The event type to remove a listener for.
  • callback (ListenerCallbackSig): The callback to remove.
Raises
  • KeyError: If the provided event type isn't found.
  • ValueError: If the provided callback isn't found.
Returns
  • Self: The client instance to enable chained calls.
#   def with_listener( self, event_type: type[hikari.events.base_events.Event], / ) -> collections.abc.Callable[[~ListenerCallbackSigT], ~ListenerCallbackSigT]:
View Source
    def with_listener(
        self, event_type: type[hikari.Event], /
    ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]:
        # <<inherited docstring from tanjun.abc.Client>>.
        def decorator(callback: tanjun_abc.ListenerCallbackSigT, /) -> tanjun_abc.ListenerCallbackSigT:
            self.add_listener(event_type, callback)
            return callback

        return decorator

Add an event listener to this client through a decorator call.

Examples
client = tanjun.Client.from_gateway_bot(bot)

@client.with_listener(hikari.MessageCreateEvent)
async def on_message_create(event: hikari.MessageCreateEvent) -> None:
    raise NotImplementedError
Parameters
  • event_type (type[hikari.Event]): The event type to listener for.
Returns
  • collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]: Decorator callback used to register the event callback.

The callback must be a coroutine function which returns None and always takes at least one positional arg of type hikari.Event regardless of client implementation detail.

#   def add_prefix( self: ~_ClientT, prefixes: Union[collections.abc.Iterable[str], str], / ) -> ~_ClientT:
View Source
    def add_prefix(self: _ClientT, prefixes: typing.Union[collections.Iterable[str], str], /) -> _ClientT:
        """Add a prefix used to filter message command calls.

        This will be matched against the first character(s) in a message's
        content to determine whether the message command search stage of
        execution should be initiated.

        Parameters
        ----------
        prefixes : typing.Union[collections.abc.Iterable[str], str]
            Either a single string or an iterable of strings to be used as
            prefixes.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        if isinstance(prefixes, str):
            if prefixes not in self._prefixes:
                self._prefixes.append(prefixes)

        else:
            self._prefixes.extend(prefix for prefix in prefixes if prefix not in self._prefixes)

        return self

Add a prefix used to filter message command calls.

This will be matched against the first character(s) in a message's content to determine whether the message command search stage of execution should be initiated.

Parameters
  • prefixes (typing.Union[collections.abc.Iterable[str], str]): Either a single string or an iterable of strings to be used as prefixes.
Returns
  • Self: The client instance to enable chained calls.
#   def remove_prefix(self: ~_ClientT, prefix: str, /) -> ~_ClientT:
View Source
    def remove_prefix(self: _ClientT, prefix: str, /) -> _ClientT:
        """Remove a message content prefix from the client.

        Parameters
        ----------
        prefix : str
            The prefix to remove.

        Raises
        ------
        ValueError
            If the prefix is not registered with the client.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._prefixes.remove(prefix)
        return self

Remove a message content prefix from the client.

Parameters
  • prefix (str): The prefix to remove.
Raises
  • ValueError: If the prefix is not registered with the client.
Returns
  • Self: The client instance to enable chained calls.
#   def set_prefix_getter( self: ~_ClientT, getter: Optional[collections.abc.Callable[..., collections.abc.Awaitable[collections.abc.Iterable[str]]]], / ) -> ~_ClientT:
View Source
    def set_prefix_getter(self: _ClientT, getter: typing.Optional[PrefixGetterSig], /) -> _ClientT:
        """Set the callback used to retrieve message prefixes set for the relevant guild.

        Parameters
        ----------
        getter : typing.Optional[PrefixGetterSig]
            The callback which'll be used to retrieve prefixes for the guild a
            message context is from. If `None` is passed here then the callback
            will be unset.

            This should be an async callback which one argument of type
            `tanjun.abc.MessageContext` and returns an iterable of string prefixes.
            Dependency injection is supported for this callback's keyword arguments.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._prefix_getter = injecting.CallbackDescriptor(getter) if getter else None
        return self

Set the callback used to retrieve message prefixes set for the relevant guild.

Parameters
  • getter (typing.Optional[PrefixGetterSig]): The callback which'll be used to retrieve prefixes for the guild a message context is from. If None is passed here then the callback will be unset.

    This should be an async callback which one argument of type tanjun.abc.MessageContext and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments.

Returns
  • Self: The client instance to enable chained calls.
#   def with_prefix_getter(self, getter: ~PrefixGetterSigT, /) -> ~PrefixGetterSigT:
View Source
    def with_prefix_getter(self, getter: PrefixGetterSigT, /) -> PrefixGetterSigT:
        """Set the prefix getter callback for this client through decorator call.

        Examples
        --------
        ```py
        client = tanjun.Client.from_rest_bot(bot)

        @client.with_prefix_getter
        async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]:
            raise NotImplementedError
        ```

        Parameters
        ----------
        getter : PrefixGetterSig
            The callback which'll be  to retrieve prefixes for the guild a
            message event is from.

            This should be an async callback which one argument of type
            `tanjun.abc.MessageContext` and returns an iterable of string prefixes.
            Dependency injection is supported for this callback's keyword arguments.

        Returns
        -------
        PrefixGetterSigT
            The registered callback.
        """
        self.set_prefix_getter(getter)
        return getter

Set the prefix getter callback for this client through decorator call.

Examples
client = tanjun.Client.from_rest_bot(bot)

@client.with_prefix_getter
async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]:
    raise NotImplementedError
Parameters
  • getter (PrefixGetterSig): The callback which'll be to retrieve prefixes for the guild a message event is from.

    This should be an async callback which one argument of type tanjun.abc.MessageContext and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments.

Returns
  • PrefixGetterSigT: The registered callback.
#   def iter_commands( self ) -> collections.abc.Iterator[tanjun.abc.ExecutableCommand[tanjun.abc.Context]]:
View Source
    def iter_commands(self) -> collections.Iterator[tanjun_abc.ExecutableCommand[tanjun_abc.Context]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        slash_commands = self.iter_slash_commands(global_only=False)
        yield from self.iter_message_commands()
        yield from slash_commands

Iterate over all the commands (both message and slash) registered to this client.

Returns
  • collections.abc.Iterator[ExecutableCommand[Context]]: Iterator of all the commands registered to this client.
#   def iter_message_commands( self ) -> collections.abc.Iterator[tanjun.abc.MessageCommand[typing.Any]]:
View Source
    def iter_message_commands(self) -> collections.Iterator[tanjun_abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(component.message_commands for component in self.components)

Iterate over all the message commands registered to this client.

Returns
  • collections.abc.Iterator[MessageCommand]: Iterator of all the message commands registered to this client.
#   def iter_slash_commands( self, *, global_only: bool = False ) -> collections.abc.Iterator[tanjun.abc.BaseSlashCommand]:
View Source
    def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Client>>.
        if global_only:
            return filter(lambda c: c.is_global, self.iter_slash_commands(global_only=False))

        return itertools.chain.from_iterable(component.slash_commands for component in self.components)

Iterate over all the slash commands registered to this client.

Parameters
  • global_only (bool): Whether to only iterate over global slash commands.
Returns
  • collections.abc.Iterator[BaseSlashCommand]: Iterator of all the slash commands registered to this client.
#   def check_message_name( self, name: str, / ) -> collections.abc.Iterator[tuple[str, tanjun.abc.MessageCommand[typing.Any]]]:
View Source
    def check_message_name(
        self, name: str, /
    ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(
            component.check_message_name(name) for component in self._components.values()
        )

Check whether a message command name is present in the current client.

Note: Dependent on implementation this may partial check name against the message command's name based on command_name.startswith(name).

Parameters
  • name (str): The name to match commands against.
Returns
  • collections.abc.Iterator[tuple[str, MessageCommand]]: Iterator of the matched command names to the matched message command objects.
#   def check_slash_name( self, name: str, / ) -> collections.abc.Iterator[tanjun.abc.BaseSlashCommand]:
View Source
    def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Client>>.
        return itertools.chain.from_iterable(
            component.check_slash_name(name) for component in self._components.values()
        )

Check whether a slash command name is present in the current client.

Note: This won't check the commands within command groups.

Parameters
  • name (str): Name to check against.
Returns
  • collections.abc.Iterator[BaseSlashCommand]: Iterator of the matched slash command objects.
#   async def close(self, *, deregister_listeners: bool = True) -> None:
View Source
    async def close(self, *, deregister_listeners: bool = True) -> None:
        """Close the client.

        Raises
        ------
        RuntimeError
            If the client isn't running.
        """
        if not self._loop:
            raise RuntimeError("Client isn't active")

        if self._is_closing:
            event = asyncio.Event()
            self.add_client_callback(ClientCallbackNames.CLOSED, event.set)
            try:
                await event.wait()
            finally:
                self.remove_client_callback(ClientCallbackNames.CLOSED, event.set)
            return

        self._is_closing = True
        await self.dispatch_client_callback(ClientCallbackNames.CLOSING)
        if deregister_listeners and self._events:
            if event_type := self._accepts.get_event_type():
                self._try_unsubscribe(self._events, event_type, self.on_message_create_event)

            self._try_unsubscribe(self._events, hikari.InteractionCreateEvent, self.on_interaction_create_event)

            for event_type_, listeners in self._listeners.items():
                for listener in listeners:
                    self._try_unsubscribe(self._events, event_type_, listener.__call__)

        if deregister_listeners and self._server:
            self._server.set_listener(hikari.CommandInteraction, None)

        await asyncio.gather(*(component.close() for component in self._components.copy().values()))

        self._loop = None
        await self.dispatch_client_callback(ClientCallbackNames.CLOSED)
        self._is_closing = False

Close the client.

Raises
  • RuntimeError: If the client isn't running.
#   async def open(self, *, register_listeners: bool = True) -> None:
View Source
    async def open(self, *, register_listeners: bool = True) -> None:
        """Start the client.

        If `mention_prefix` was passed to `Client.__init__` or
        `Client.from_gateway_bot` then this function may make a fetch request
        to Discord if it cannot get the current user from the cache.

        Raises
        ------
        RuntimeError
            If the client is already active.
        """
        if self._loop:
            raise RuntimeError("Client is already alive")

        self._loop = asyncio.get_running_loop()
        self._is_closing = False
        await self.dispatch_client_callback(ClientCallbackNames.STARTING)

        if self._grab_mention_prefix:
            user: typing.Optional[hikari.OwnUser] = None
            if self._cache:
                user = self._cache.get_me()

            if not user and (user_cache := self.get_type_dependency(dependencies.SingleStoreCache[hikari.OwnUser])):
                user = await user_cache.get(default=None)

            if not user:
                user = await self._rest.fetch_my_user()

            for prefix in f"<@{user.id}>", f"<@!{user.id}>":
                if prefix not in self._prefixes:
                    self._prefixes.append(prefix)

            self._grab_mention_prefix = False

        await asyncio.gather(*(component.open() for component in self._components.copy().values()))

        if register_listeners and self._events:
            if event_type := self._accepts.get_event_type():
                self._events.subscribe(event_type, self.on_message_create_event)

            self._events.subscribe(hikari.InteractionCreateEvent, self.on_interaction_create_event)

            for event_type_, listeners in self._listeners.items():
                for listener in listeners:
                    self._events.subscribe(event_type_, listener.__call__)

        if register_listeners and self._server:
            self._server.set_listener(hikari.CommandInteraction, self.on_interaction_create_request)

        self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.STARTED))

Start the client.

If mention_prefix was passed to Client.__init__ or Client.from_gateway_bot then this function may make a fetch request to Discord if it cannot get the current user from the cache.

Raises
  • RuntimeError: If the client is already active.
#   async def fetch_rest_application_id(self) -> hikari.snowflakes.Snowflake:
View Source
    async def fetch_rest_application_id(self) -> hikari.Snowflake:
        """Fetch the ID of the application this client is linked to.

        Returns
        -------
        hikari.Snowflake
            The application ID of the application this client is linked to.
        """
        if self._cached_application_id:
            return self._cached_application_id

        application_cache = self.get_type_dependency(
            dependencies.SingleStoreCache[hikari.Application]
        ) or self.get_type_dependency(dependencies.SingleStoreCache[hikari.AuthorizationApplication])
        if application_cache and (application := await application_cache.get(default=None)):
            self._cached_application_id = application.id
            return application.id

        if self._rest.token_type == hikari.TokenType.BOT:
            self._cached_application_id = hikari.Snowflake(await self._rest.fetch_application())

        else:
            self._cached_application_id = hikari.Snowflake((await self._rest.fetch_authorization()).application)

        return self._cached_application_id

Fetch the ID of the application this client is linked to.

Returns
  • hikari.Snowflake: The application ID of the application this client is linked to.
#   def set_hooks( self: ~_ClientT, hooks: Optional[tanjun.abc.Hooks[tanjun.abc.Context]], / ) -> ~_ClientT:
View Source
    def set_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ClientT:
        """Set the general command execution hooks for this client.

        The callbacks within this hook will be added to every slash and message
        command execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.AnyHooks]
            The general command execution hooks to set for this client.

            Passing `None` will remove all hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._hooks = hooks
        return self

Set the general command execution hooks for this client.

The callbacks within this hook will be added to every slash and message command execution started by this client.

Parameters
  • hooks (typing.Optional[tanjun_abc.AnyHooks]): The general command execution hooks to set for this client.

    Passing None will remove all hooks.

Returns
  • Self: The client instance to enable chained calls.
#   def set_slash_hooks( self: ~_ClientT, hooks: Optional[tanjun.abc.Hooks[tanjun.abc.SlashContext]], / ) -> ~_ClientT:
View Source
    def set_slash_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.SlashHooks], /) -> _ClientT:
        """Set the slash command execution hooks for this client.

        The callbacks within this hook will be added to every slash command
        execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.SlashHooks]
            The slash context specific command execution hooks to set for this
            client.

            Passing `None` will remove the hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._slash_hooks = hooks
        return self

Set the slash command execution hooks for this client.

The callbacks within this hook will be added to every slash command execution started by this client.

Parameters
  • hooks (typing.Optional[tanjun_abc.SlashHooks]): The slash context specific command execution hooks to set for this client.

    Passing None will remove the hooks.

Returns
  • Self: The client instance to enable chained calls.
#   def set_message_hooks( self: ~_ClientT, hooks: Optional[tanjun.abc.Hooks[tanjun.abc.MessageContext]], / ) -> ~_ClientT:
View Source
    def set_message_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.MessageHooks], /) -> _ClientT:
        """Set the message command execution hooks for this client.

        The callbacks within this hook will be added to every message command
        execution started by this client.

        Parameters
        ----------
        hooks : typing.Optional[tanjun_abc.MessageHooks]
            The message context specific command execution hooks to set for this
            client.

            Passing `None` will remove all hooks.

        Returns
        -------
        Self
            The client instance to enable chained calls.
        """
        self._message_hooks = hooks
        return self

Set the message command execution hooks for this client.

The callbacks within this hook will be added to every message command execution started by this client.

Parameters
  • hooks (typing.Optional[tanjun_abc.MessageHooks]): The message context specific command execution hooks to set for this client.

    Passing None will remove all hooks.

Returns
  • Self: The client instance to enable chained calls.
#   def load_modules(self: ~_ClientT, *modules: Union[str, pathlib.Path]) -> ~_ClientT:
View Source
    def load_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = module_path.absolute()

            generator = self._load_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = load_module()

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

        return self

Load entities into this client from modules based on present loaders.

Note: If an __all__ is present in the target module then it will be used to find loaders.

Examples

For this to work the target module has to have at least one loader present.

@tanjun.as_loader
def load_module(client: tanjun.Client) -> None:
    client.add_component(component.copy())

or

loader = tanjun.Component("trans component").load_from_scope().make_loader()
Parameters
  • *modules (typing.Union[str, pathlib.Path]): Path(s) of the modules to load from.

    When str this will be treated as a normal import path which is absolute ("foo.bar.baz"). It's worth noting that absolute module paths may be imported from the current location if the top level module is a valid module file or module directory in the current working directory.

    When pathlib.Path the module will be imported directly from the given path. In this mode any relative imports in the target module will fail to resolve.

Returns
  • Self: This client instance to enable chained calls.
Raises

This includes if it failed to import or if one of its loaders raised. The source error can be found at tanjun.errors.FailedModuleLoad.__source__.

#   async def load_modules_async(self, *modules: Union[str, pathlib.Path]) -> None:
View Source
    async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        loop = asyncio.get_running_loop()
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = await loop.run_in_executor(None, module_path.absolute)

            generator = self._load_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = await loop.run_in_executor(None, load_module)

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

Asynchronous variant of Client.load_modules.

Unlike Client.load_modules, this method will run blocking code in a background thread.

For more information on the behaviour of this method see the documentation for Client.load_modules.

#   def unload_modules(self: ~_ClientT, *modules: Union[str, pathlib.Path]) -> ~_ClientT:
View Source
    def unload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.ab.Client>>.
        for module_path in modules:
            if isinstance(module_path, str):
                modules_dict: dict[typing.Any, types.ModuleType] = self._modules

            else:
                modules_dict = self._path_modules
                module_path = module_path.absolute()

            module = modules_dict.get(module_path)
            if not module:
                raise errors.ModuleStateConflict(f"Module {module_path!s} not loaded", module_path)

            _LOGGER.info("Unloading from %s", module_path)
            with _WrapLoadError(errors.FailedModuleUnload):
                self._call_unloaders(module_path, _get_loaders(module, module_path))

            del modules_dict[module_path]

        return self

Unload entities from this client based on unloaders in one or more modules.

Note: If an __all__ is present in the target module then it will be used to find unloaders.

Examples

For this to work the module has to have at least one unloading enabled tanjun.abc.ClientLoader present.

@tanjun.as_unloader
def unload_component(client: tanjun.Client) -> None:
    client.remove_component_by_name(component.name)

or

# as_loader's returned ClientLoader handles both loading and unloading.
loader = tanjun.Component("trans component").load_from_scope().as_loader(unload_component)
Parameters
  • *modules (typing.Union[str, pathlib.Path]): Path of one or more modules to unload.

    These should be the same path(s) which were passed to load_module.

Returns
  • Self: This client instance to enable chained calls.
Raises

This indicates that one of its unloaders raised. The source error can be found at tanjun.errors.FailedModuleUnload.__source__.

#   def reload_modules(self: ~_ClientT, *modules: Union[str, pathlib.Path]) -> ~_ClientT:
View Source
    def reload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT:
        # <<inherited docstring from tanjun.abc.Client>>.
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = module_path.absolute()

            generator = self._reload_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = load_module()

            try:
                generator.send(module)
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't finish")

        return self

Reload entities in this client based on the loaders in loaded module(s).

Note: If an __all__ is present in the target module then it will be used to find loaders and unloaders.

Examples

For this to work the module has to have at least one ClientLoader which handles loading and one which handles unloading present.

Parameters
  • *modules (typing.Union[str, pathlib.Path]): Paths of one or more module to unload.

    These should be the same paths which were passed to load_module.

Returns
  • Self: This client instance to enable chained calls.
Raises

This includes if it failed to import or if one of its loaders raised. The source error can be found at tanjun.errors.FailedModuleLoad.__source__.

This indicates that one of its unloaders raised. The source error can be found at tanjun.errors.FailedModuleUnload.__source__.

#   async def reload_modules_async(self, *modules: Union[str, pathlib.Path]) -> None:
View Source
    async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None:
        # <<inherited docstring from tanjun.abc.Client>>.
        loop = asyncio.get_running_loop()
        for module_path in modules:
            if isinstance(module_path, pathlib.Path):
                module_path = await loop.run_in_executor(None, module_path.absolute)

            generator = self._reload_module(module_path)
            load_module = next(generator)
            with _WrapLoadError(errors.FailedModuleLoad):
                module = await loop.run_in_executor(None, load_module)

            try:
                generator.send(module)

            except StopIteration:
                pass

            else:
                raise RuntimeError("Generator didn't finish")

Asynchronous variant of Client.reload_modules.

Unlike Client.reload_modules, this method will run blocking code in a background thread.

For more information on the behaviour of this method see the documentation for Client.reload_modules.

#   async def on_message_create_event( self, event: hikari.events.message_events.MessageCreateEvent, / ) -> None:
View Source
    async def on_message_create_event(self, event: hikari.MessageCreateEvent, /) -> None:
        """Execute a message command based on a gateway event.

        Parameters
        ----------
        hikari.events.message_events.MessageCreateEvent
            The event to handle.
        """
        if event.message.content is None:
            return

        ctx = self._make_message_context(
            client=self, injection_client=self, content=event.message.content, message=event.message
        )
        if (prefix := await self._check_prefix(ctx)) is None:
            return

        ctx.set_content(ctx.content.lstrip()[len(prefix) :].lstrip()).set_triggering_prefix(prefix)
        hooks: typing.Optional[set[tanjun_abc.MessageHooks]] = None
        if self._hooks and self._message_hooks:
            hooks = {self._hooks, self._message_hooks}

        elif self._hooks:
            hooks = {self._hooks}

        elif self._message_hooks:
            hooks = {self._message_hooks}

        try:
            if await self.check(ctx):
                for component in self._components.values():
                    if await component.execute_message(ctx, hooks=hooks):
                        return

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            return

        await self.dispatch_client_callback(ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND, ctx)

Execute a message command based on a gateway event.

Parameters
  • hikari.events.message_events.MessageCreateEvent: The event to handle.
#   async def on_interaction_create_event( self, event: hikari.events.interaction_events.InteractionCreateEvent, / ) -> None:
View Source
    async def on_interaction_create_event(self, event: hikari.InteractionCreateEvent, /) -> None:
        """Execute a slash command based on Gateway events.

        .. note::
            Any event where `event.interaction` is not
            `hikari.CommandInteraction` will be ignored.

        Parameters
        ----------
        event : hikari.events.interaction_events.InteractionCreateEvent
            The event to execute commands based on.
        """
        if not isinstance(event.interaction, hikari.CommandInteraction):
            return

        ctx = self._make_slash_context(
            client=self,
            injection_client=self,
            interaction=event.interaction,
            on_not_found=self._on_slash_not_found,
            default_to_ephemeral=self._defaults_to_ephemeral,
        )
        hooks = self._get_slash_hooks()

        if self._auto_defer_after is not None:
            ctx.start_defer_timer(self._auto_defer_after)

        try:
            if await self.check(ctx):
                for component in self._components.values():
                    # This is set on each iteration to ensure that any component
                    # state which was set to this isn't propagated to other components.
                    ctx.set_ephemeral_default(self._defaults_to_ephemeral)
                    if future := await component.execute_interaction(ctx, hooks=hooks):
                        await future
                        return

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            return

        await ctx.mark_not_found()

Execute a slash command based on Gateway events.

Note: Any event where event.interaction is not hikari.CommandInteraction will be ignored.

Parameters
  • event (hikari.events.interaction_events.InteractionCreateEvent): The event to execute commands based on.
#   async def on_interaction_create_request( self, interaction: hikari.interactions.command_interactions.CommandInteraction, / ) -> Union[hikari.api.special_endpoints.InteractionMessageBuilder, hikari.api.special_endpoints.InteractionDeferredBuilder]:
View Source
    async def on_interaction_create_request(self, interaction: hikari.CommandInteraction, /) -> context.ResponseTypeT:
        """Execute a slash command based on received REST requests.

        Parameters
        ----------
        interaction : hikari.CommandInteraction
            The interaction to execute a command based on.

        Returns
        -------
        tanjun.context.ResponseType
            The initial response to send back to Discord.
        """
        ctx = self._make_slash_context(
            client=self,
            injection_client=self,
            interaction=interaction,
            on_not_found=self._on_slash_not_found,
            default_to_ephemeral=self._defaults_to_ephemeral,
        )
        if self._auto_defer_after is not None:
            ctx.start_defer_timer(self._auto_defer_after)

        hooks = self._get_slash_hooks()
        future = ctx.get_response_future()
        try:
            if await self.check(ctx):
                for component in self._components.values():
                    # This is set on each iteration to ensure that any component
                    # state which was set to this isn't propagated to other components.
                    ctx.set_ephemeral_default(self._defaults_to_ephemeral)
                    if await component.execute_interaction(ctx, hooks=hooks):
                        return await future

        except errors.HaltExecution:
            pass

        except errors.CommandError as exc:
            # Under very specific timing there may be another future which could set a result while we await
            # ctx.respond therefore we create a task to avoid any erroneous behaviour from this trying to create
            # another response before it's returned the initial response.
            asyncio.get_running_loop().create_task(
                ctx.respond(exc.message), name=f"{interaction.id} command error responder"
            )
            return await future

        asyncio.get_running_loop().create_task(ctx.mark_not_found(), name=f"{interaction.id} not found")
        return await future

Execute a slash command based on received REST requests.

Parameters
  • interaction (hikari.CommandInteraction): The interaction to execute a command based on.
Returns
#   class MessageAcceptsEnum(builtins.str, enum.Enum):
View Source
class MessageAcceptsEnum(str, enum.Enum):
    """The possible configurations for which events `Client` should execute commands based on."""

    ALL = "ALL"
    """Set the client to execute commands based on both DM and guild message create events."""

    DM_ONLY = "DM_ONLY"
    """Set the client to execute commands based only DM message create events."""

    GUILD_ONLY = "GUILD_ONLY"
    """Set the client to execute commands based only guild message create events."""

    NONE = "NONE"
    """Set the client to not execute commands based on message create events."""

    def get_event_type(self) -> typing.Optional[type[hikari.MessageCreateEvent]]:
        """Get the base event type this mode listens to.

        Returns
        -------
        typing.Optional[type[hikari.message_events.MessageCreateEvent]]
            The type object of the MessageCreateEvent class this mode will
            register a listener for.

            This will be `None` if this mode disables listening to
            message create events.
        """
        return _ACCEPTS_EVENT_TYPE_MAPPING[self]

The possible configurations for which events Client should execute commands based on.

Set the client to execute commands based on both DM and guild message create events.

#   DM_ONLY = <MessageAcceptsEnum.DM_ONLY: 'DM_ONLY'>

Set the client to execute commands based only DM message create events.

#   GUILD_ONLY = <MessageAcceptsEnum.GUILD_ONLY: 'GUILD_ONLY'>

Set the client to execute commands based only guild message create events.

#   NONE = <MessageAcceptsEnum.NONE: 'NONE'>

Set the client to not execute commands based on message create events.

#   def get_event_type( self ) -> Optional[type[hikari.events.message_events.MessageCreateEvent]]:
View Source
    def get_event_type(self) -> typing.Optional[type[hikari.MessageCreateEvent]]:
        """Get the base event type this mode listens to.

        Returns
        -------
        typing.Optional[type[hikari.message_events.MessageCreateEvent]]
            The type object of the MessageCreateEvent class this mode will
            register a listener for.

            This will be `None` if this mode disables listening to
            message create events.
        """
        return _ACCEPTS_EVENT_TYPE_MAPPING[self]

Get the base event type this mode listens to.

Returns
  • typing.Optional[type[hikari.message_events.MessageCreateEvent]]: The type object of the MessageCreateEvent class this mode will register a listener for.

This will be None if this mode disables listening to message create events.

Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Standard implementation of Tanjun's command objects."""
from __future__ import annotations

__all__: list[str] = [
    "AnyMessageCommandT",
    "ConverterSig",
    "as_message_command",
    "as_message_command_group",
    "as_slash_command",
    "slash_command_group",
    "MessageCommand",
    "MessageCommandGroup",
    "PartialCommand",
    "BaseSlashCommand",
    "SlashCommand",
    "SlashCommandGroup",
    "with_str_slash_option",
    "with_int_slash_option",
    "with_float_slash_option",
    "with_bool_slash_option",
    "with_role_slash_option",
    "with_user_slash_option",
    "with_member_slash_option",
    "with_channel_slash_option",
    "with_mentionable_slash_option",
]

import copy
import re
import typing
import warnings
from collections import abc as collections

import hikari

from . import abc
from . import checks as checks_
from . import components
from . import conversion
from . import errors
from . import hooks as hooks_
from . import injecting
from . import utilities

if typing.TYPE_CHECKING:
    from hikari.api import special_endpoints as special_endpoints_api

    _MessageCommandT = typing.TypeVar("_MessageCommandT", bound="MessageCommand[typing.Any]")
    _MessageCommandGroupT = typing.TypeVar("_MessageCommandGroupT", bound="MessageCommandGroup[typing.Any]")
    _PartialCommandT = typing.TypeVar("_PartialCommandT", bound="PartialCommand[typing.Any]")
    _BaseSlashCommandT = typing.TypeVar("_BaseSlashCommandT", bound="BaseSlashCommand")
    _SlashCommandT = typing.TypeVar("_SlashCommandT", bound="SlashCommand[typing.Any]")
    _SlashCommandGroupT = typing.TypeVar("_SlashCommandGroupT", bound="SlashCommandGroup")


_CallbackishT = typing.Union[
    abc.CommandCallbackSigT,
    abc.MessageCommand[abc.CommandCallbackSigT],
    abc.SlashCommand[abc.CommandCallbackSigT],
]

AnyMessageCommandT = typing.TypeVar("AnyMessageCommandT", bound=abc.MessageCommand[typing.Any])
ConverterSig = collections.Callable[..., abc.MaybeAwaitableT[typing.Any]]
"""Type hint of a converter used for a slash command option."""
_EMPTY_DICT: typing.Final[dict[typing.Any, typing.Any]] = {}
_EMPTY_HOOKS: typing.Final[hooks_.Hooks[typing.Any]] = hooks_.Hooks()


class PartialCommand(abc.ExecutableCommand[abc.ContextT], components.AbstractComponentLoader):
    """Base class for the standard ExecutableCommand implementations."""

    __slots__ = ("_checks", "_component", "_hooks", "_metadata")

    def __init__(self) -> None:
        self._checks: list[checks_.InjectableCheck] = []
        self._component: typing.Optional[abc.Component] = None
        self._hooks: typing.Optional[abc.Hooks[abc.ContextT]] = None
        self._metadata: dict[typing.Any, typing.Any] = {}

    @property
    def checks(self) -> collections.Collection[abc.CheckSig]:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        return tuple(check.callback for check in self._checks)

    @property
    def component(self) -> typing.Optional[abc.Component]:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        return self._component

    @property
    def hooks(self) -> typing.Optional[abc.Hooks[abc.ContextT]]:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        return self._hooks

    @property
    def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        return self._metadata

    @property
    def needs_injector(self) -> bool:
        # <<inherited docstring from tanjun.injecting.Injectable>>.
        return any(check.needs_injector for check in self._checks)

    def copy(self: _PartialCommandT, *, _new: bool = True) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._checks = [check.copy() for check in self._checks]
            self._hooks = self._hooks.copy() if self._hooks else None
            self._metadata = self._metadata.copy()
            return self

        return copy.copy(self).copy(_new=False)

    def set_hooks(self: _PartialCommandT, hooks: typing.Optional[abc.Hooks[abc.ContextT]], /) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        self._hooks = hooks
        return self

    def set_metadata(self: _PartialCommandT, key: typing.Any, value: typing.Any, /) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        self._metadata[key] = value
        return self

    def add_check(self: _PartialCommandT, check: abc.CheckSig, /) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if check not in self._checks:
            self._checks.append(checks_.InjectableCheck(check))

        return self

    def remove_check(self: _PartialCommandT, check: abc.CheckSig, /) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        self._checks.remove(typing.cast("checks_.InjectableCheck", check))
        return self

    def with_check(self, check: abc.CheckSigT, /) -> abc.CheckSigT:
        self.add_check(check)
        return check

    def bind_client(self: _PartialCommandT, client: abc.Client, /) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        return self

    def bind_component(self: _PartialCommandT, component: abc.Component, /) -> _PartialCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        self._component = component
        return self


_SCOMMAND_NAME_REG: typing.Final[re.Pattern[str]] = re.compile(r"^[\w-]{1,32}$", flags=re.UNICODE)


def _validate_name(name: str) -> None:
    if not _SCOMMAND_NAME_REG.fullmatch(name):
        raise ValueError(f"Invalid name provided, {name!r} doesn't match the required regex `^\\w{{1,32}}$`")

    if name.lower() != name:
        raise ValueError(f"Invalid name provided, {name!r} must be lowercase")


def slash_command_group(
    name: str,
    description: str,
    /,
    *,
    default_permission: bool = True,
    default_to_ephemeral: typing.Optional[bool] = None,
    is_global: bool = True,
) -> SlashCommandGroup:
    r"""Create a slash command group.

    Examples
    --------
    Sub-commands can be added to the created slash command object through
    the following decorator based approach:
    ```python
    help_group = tanjun.slash_command_group("help", "get help")

    @help_group.with_command
    @tanjun.with_str_slash_option("command_name", "command name")
    @tanjun.as_slash_command("command", "Get help with a command")
    async def help_command_command(ctx: tanjun.abc.SlashContext, command_name: str) -> None:
        ...

    @help_group.with_command
    @tanjun.as_slash_command("me", "help me")
    async def help_me_command(ctx: tanjun.abc.SlashContext) -> None:
        ...

    component = tanjun.Component().add_slash_command(help_group)
    ```

    Notes
    -----
    * Unlike message command grups, slash command groups cannot
      be callable functions themselves.
    * Under the standard implementation, `is_global` is used to determine whether
      the command should be bulk set by `tanjun.Client.set_global_commands`
      or when `set_global_commands` is True

    Parameters
    ----------
    name : str
        The name of the command group.

        This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase.
    description : str
        The description of the command group.

    Other Parameters
    ----------------
    default_permission : bool
        Whether this command can be accessed without set permissions.

        Defaults to `True`, meaning that users can access the command by default.
    default_to_ephemeral : typing.Optional[bool]
        Whether this command's responses should default to ephemeral unless flags
        are set to override this.

        If this is left as `None` then the default set on the parent command(s),
        component or client will be in effect.
    is_global : bool
        Whether this command is a global command. Defaults to `True`.

    Returns
    -------
    SlashCommandGroup
        The command group.

    Raises
    ------
    ValueError
        Raises a value error for any of the following reasons:
        * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
        * If the command name has uppercase characters.
        * If the description is over 100 characters long.
    """
    return SlashCommandGroup(
        name,
        description,
        default_permission=default_permission,
        default_to_ephemeral=default_to_ephemeral,
        is_global=is_global,
    )


def as_slash_command(
    name: str,
    description: str,
    /,
    *,
    always_defer: bool = False,
    default_permission: bool = True,
    default_to_ephemeral: typing.Optional[bool] = None,
    is_global: bool = True,
    sort_options: bool = True,
) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], SlashCommand[abc.CommandCallbackSigT]]:
    r"""Build a `SlashCommand` by decorating a function.

    .. note::
        Under the standard implementation, `is_global` is used to determine whether
        the command should be bulk set by `tanjun.Client.set_global_commands`
        or when `set_global_commands` is True

    .. warning::
        `default_permission` and `is_global` are ignored for commands within
        slash command groups.

    Examples
    --------
    ```py
    @as_slash_command("ping", "Get the bot's latency")
    async def ping_command(self, ctx: tanjun.abc.SlashContext) -> None:
        start_time = time.perf_counter()
        await ctx.rest.fetch_my_user()
        time_taken = (time.perf_counter() - start_time) * 1_000
        await ctx.respond(f"PONG\n - REST: {time_taken:.0f}mss")
    ```

    Parameters
    ----------
    name : str
        The command's name.

        This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
    description : str
        The command's description.
        This should be inclusively between 1-100 characters in length.

    Other Parameters
    ----------------
    always_defer : bool
        Whether the contexts this command is executed with should always be deferred
        before being passed to the command's callback.

        Defaults to `False`.

        .. note::
            The ephemeral state of the first response is decided by whether the
            deferral is ephemeral.
    default_permission : bool
        Whether this command can be accessed without set permissions.

        Defaults to `True`, meaning that users can access the command by default.
    default_to_ephemeral : typing.Optional[bool]
        Whether this command's responses should default to ephemeral unless flags
        are set to override this.

        If this is left as `None` then the default set on the parent command(s),
        component or client will be in effect.
    is_global : bool
        Whether this command is a global command. Defaults to `True`.
    sort_options : bool
        Whether this command should sort its set options based on whether
        they're required.

        If this is `True` then the options are re-sorted to meet the requirement
        from Discord that required command options be listed before optional
        ones.

    Returns
    -------
    collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], SlashCommand[CommandCallbackSigT]]
        The decorator callback used to make a `SlashCommand`.

        This can either wrap a raw command callback or another callable command instance
        (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage
        loading the other command into a component when using `tanjun.Component.load_from_scope`.

    Raises
    ------
    ValueError
        Raises a value error for any of the following reasons:
        * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
        * If the command name has uppercase characters.
        * If the description is over 100 characters long.
    """

    def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> SlashCommand[abc.CommandCallbackSigT]:
        if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)):
            return SlashCommand(
                callback.callback,
                name,
                description,
                always_defer=always_defer,
                default_permission=default_permission,
                default_to_ephemeral=default_to_ephemeral,
                is_global=is_global,
                sort_options=sort_options,
                _wrapped_command=callback,
            )

        return SlashCommand(
            callback,
            name,
            description,
            always_defer=always_defer,
            default_permission=default_permission,
            default_to_ephemeral=default_to_ephemeral,
            is_global=is_global,
            sort_options=sort_options,
        )

    return decorator


_UNDEFINED_DEFAULT = object()


def with_str_slash_option(
    name: str,
    description: str,
    /,
    *,
    choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None,
    converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (),
    default: typing.Any = _UNDEFINED_DEFAULT,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a string option to a slash command.

    For more information on this function's parameters see `SlashCommand.add_str_option`.

    Examples
    --------
    ```py
    @with_str_slash_option("name", "A name.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, name: str) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_str_option(
        name,
        description,
        default=default,
        choices=choices,
        converters=converters,
        pass_as_kwarg=pass_as_kwarg,
        _stack_level=1,
    )


def with_int_slash_option(
    name: str,
    description: str,
    /,
    *,
    choices: typing.Optional[collections.Mapping[str, int]] = None,
    converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
    default: typing.Any = _UNDEFINED_DEFAULT,
    min_value: typing.Optional[int] = None,
    max_value: typing.Optional[int] = None,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add an integer option to a slash command.

    For information on this function's parameters see `SlashCommand.add_int_option`.

    Examples
    --------
    ```py
    @with_int_slash_option("int_value", "Int value.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, int_value: int) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_int_option(
        name,
        description,
        default=default,
        choices=choices,
        converters=converters,
        min_value=min_value,
        max_value=max_value,
        pass_as_kwarg=pass_as_kwarg,
        _stack_level=1,
    )


def with_float_slash_option(
    name: str,
    description: str,
    /,
    *,
    always_float: bool = True,
    choices: typing.Optional[collections.Mapping[str, float]] = None,
    converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
    default: typing.Any = _UNDEFINED_DEFAULT,
    min_value: typing.Optional[float] = None,
    max_value: typing.Optional[float] = None,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a float option to a slash command.

    For information on this function's parameters see `SlashCommand.add_float_option`.

    Examples
    --------
    ```py
    @with_float_slash_option("float_value", "Float value.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, float_value: float) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_float_option(
        name,
        description,
        always_float=always_float,
        default=default,
        choices=choices,
        converters=converters,
        min_value=min_value,
        max_value=max_value,
        pass_as_kwarg=pass_as_kwarg,
        _stack_level=1,
    )


def with_bool_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a boolean option to a slash command.

    For information on this function's parameters see `SlashContext.add_bool_option`.

    Examples
    --------
    ```py
    @with_bool_slash_option("flag", "Whether this flag should be enabled.", default=False)
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, flag: bool) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_bool_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)


def with_user_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a user option to a slash command.

    For information on this function's parameters see `SlashContext.add_user_option`.

    .. note::
        This may result in `hikari.InteractionMember` or
        `hikari.users.User` if the user isn't in the current guild or if this
        command was executed in a DM channel.

    Examples
    --------
    ```py
    @with_user_slash_option("user", "user to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, user: Union[InteractionMember, User]) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_user_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)


def with_member_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a member option to a slash command.

    For information on this function's arguments see `SlashCommand.add_member_option`.

    .. note::
        This will always result in `hikari.InteractionMember`.

    Examples
    --------
    ```py
    @with_member_slash_option("member", "member to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, member: hikari.InteractionMember) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_member_option(name, description, default=default)


_channel_types: dict[type[hikari.PartialChannel], set[hikari.ChannelType]] = {
    hikari.GuildTextChannel: {hikari.ChannelType.GUILD_TEXT},
    hikari.DMChannel: {hikari.ChannelType.DM},
    hikari.GuildVoiceChannel: {hikari.ChannelType.GUILD_VOICE},
    hikari.GroupDMChannel: {hikari.ChannelType.GROUP_DM},
    hikari.GuildCategory: {hikari.ChannelType.GUILD_CATEGORY},
    hikari.GuildNewsChannel: {hikari.ChannelType.GUILD_NEWS},
    hikari.GuildStoreChannel: {hikari.ChannelType.GUILD_STORE},
    hikari.GuildStageChannel: {hikari.ChannelType.GUILD_STAGE},
}


for _channel_cls, _types in _channel_types.copy().items():
    for _mro_type in _channel_cls.mro():
        if isinstance(_mro_type, type) and issubclass(_mro_type, hikari.PartialChannel):
            try:
                _channel_types[_mro_type].update(_types)
            except KeyError:
                _channel_types[_mro_type] = _types.copy()


def with_channel_slash_option(
    name: str,
    description: str,
    /,
    *,
    types: typing.Union[collections.Collection[type[hikari.PartialChannel]], None] = None,
    default: typing.Any = _UNDEFINED_DEFAULT,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a channel option to a slash command.

    For information on this function's parameters see `SlashCommand.add_channel_option`.

    .. note::
        This will always result in `hikari..InteractionChannel`.

    Examples
    --------
    ```py
    @with_channel_slash_option("channel", "channel to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, channel: hikari.InteractionChannel) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_channel_option(name, description, types=types, default=default, pass_as_kwarg=pass_as_kwarg)


def with_role_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a role option to a slash command.

    For information on this function's parameters see `SlashCommand.add_role_option`.

    Examples
    --------
    ```py
    @with_role_slash_option("role", "Role to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, role: hikari.Role) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_role_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)


def with_mentionable_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a mentionable option to a slash command.

    For information on this function's arguments see `SlashCommand.add_mentionable_option`.

    .. note::
        This may target roles, guild members or users and results in
        `Union[hikari.User, hikari.InteractionMember, hikari.Role]`.

    Examples
    --------
    ```py
    @with_mentionable_slash_option("mentionable", "Mentionable entity to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, mentionable: [Role, InteractionMember, User]) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_mentionable_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)


def _convert_to_injectable(converter: ConverterSig) -> injecting.CallbackDescriptor[typing.Any]:
    if isinstance(converter, injecting.CallbackDescriptor):
        return typing.cast("injecting.CallbackDescriptor[typing.Any]", converter)

    return injecting.CallbackDescriptor(conversion.override_type(converter))


class _TrackedOption:
    __slots__ = ("converters", "default", "is_always_float", "is_only_member", "name", "type")

    def __init__(
        self,
        *,
        name: str,
        option_type: typing.Union[hikari.OptionType, int],
        always_float: bool = False,
        converters: typing.Optional[list[injecting.CallbackDescriptor[typing.Any]]] = None,
        only_member: bool = False,
        default: typing.Any = _UNDEFINED_DEFAULT,
    ) -> None:
        self.converters = converters or []
        self.default = default
        self.is_always_float = always_float
        self.is_only_member = only_member
        self.name = name
        self.type = option_type

    @property
    def needs_injector(self) -> bool:
        return any(converter.needs_injector for converter in self.converters)

    def check_client(self, client: abc.Client, /) -> None:
        for converter in self.converters:
            if isinstance(converter.callback, conversion.BaseConverter):
                converter.callback.check_client(client, f"{self.name} slash command option")

    async def convert(self, ctx: abc.SlashContext, value: typing.Any, /) -> typing.Any:
        if not self.converters:
            return value

        exceptions: list[ValueError] = []
        for converter in self.converters:
            try:
                return await converter.resolve_with_command_context(ctx, value)

            except ValueError as exc:
                exceptions.append(exc)

        raise errors.ConversionError(f"Couldn't convert {self.type} '{self.name}'", self.name, errors=exceptions)


_CommandBuilderT = typing.TypeVar("_CommandBuilderT", bound="_CommandBuilder")


class _CommandBuilder(hikari.impl.CommandBuilder):
    __slots__ = ("_has_been_sorted", "_sort_options")

    def __init__(
        self,
        name: str,
        description: str,
        sort_options: bool,
        *,
        id: hikari.UndefinedOr[hikari.Snowflake] = hikari.UNDEFINED,  # noqa: A002
    ) -> None:
        super().__init__(name, description, id=id)  # type: ignore
        self._has_been_sorted = True
        self._sort_options = sort_options

    def add_option(self: _CommandBuilderT, option: hikari.CommandOption) -> _CommandBuilderT:
        if self._options:
            self._has_been_sorted = False

        super().add_option(option)
        return self

    def sort(self: _CommandBuilderT) -> _CommandBuilderT:
        if self._sort_options and not self._has_been_sorted:
            required: list[hikari.CommandOption] = []
            not_required: list[hikari.CommandOption] = []
            for option in self._options:
                if option.is_required:
                    required.append(option)
                else:
                    not_required.append(option)

            self._options = [*required, *not_required]
            self._has_been_sorted = True

        return self

    def copy(self) -> _CommandBuilder:  # TODO: can we just del _CommandBuilder.__copy__ to go back to the default?
        builder = _CommandBuilder(self.name, self.description, self._sort_options, id=self.id)

        for option in self._options:
            builder.add_option(option)

        return builder


class BaseSlashCommand(PartialCommand[abc.SlashContext], abc.BaseSlashCommand):
    """Base class used for the standard slash command implementations."""

    __slots__ = ("_defaults_to_ephemeral", "_description", "_is_global", "_name", "_parent", "_tracked_command")

    def __init__(
        self,
        name: str,
        description: str,
        /,
        *,
        default_to_ephemeral: typing.Optional[bool] = None,
        is_global: bool = True,
    ) -> None:
        super().__init__()
        _validate_name(name)
        if len(description) > 100:
            raise ValueError("The command description cannot be over 100 characters in length")

        self._defaults_to_ephemeral = default_to_ephemeral
        self._description = description
        self._is_global = is_global
        self._name = name
        self._parent: typing.Optional[abc.SlashCommandGroup] = None
        self._tracked_command: typing.Optional[hikari.Command] = None

    @property
    def defaults_to_ephemeral(self) -> typing.Optional[bool]:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._defaults_to_ephemeral

    @property
    def description(self) -> str:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._description

    @property
    def is_global(self) -> bool:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._is_global

    @property
    def name(self) -> str:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._name

    @property
    def parent(self) -> typing.Optional[abc.SlashCommandGroup]:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._parent

    @property
    def tracked_command(self) -> typing.Optional[hikari.Command]:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._tracked_command

    @property
    def tracked_command_id(self) -> typing.Optional[hikari.Snowflake]:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._tracked_command.id if self._tracked_command else None

    def set_tracked_command(self: _BaseSlashCommandT, command: hikari.Command, /) -> _BaseSlashCommandT:
        """Set the the global command this should be tracking.

        Parameters
        ----------
        command : hikari.Command
            object of the global command this should be tracking.

        Returns
        -------
        SelfT
            This command instance for chaining.
        """
        self._tracked_command = command
        return self

    def set_ephemeral_default(self: _BaseSlashCommandT, state: typing.Optional[bool], /) -> _BaseSlashCommandT:
        """Set whether this command's responses should default to ephemeral.

        Parameters
        ----------
        typing.Optional[bool]
            Whether this command's responses should default to ephemeral.
            This will be overridden by any response calls which specify flags.

            Setting this to `None` will let the default set on the parent
            command(s), component or client propagate and decide the ephemeral
            default for contexts used by this command.

        Returns
        -------
        SelfT
            This command to allow for chaining.
        """
        self._defaults_to_ephemeral = state
        return self

    def set_parent(self: _BaseSlashCommandT, parent: typing.Optional[abc.SlashCommandGroup], /) -> _BaseSlashCommandT:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        self._parent = parent
        return self

    async def check_context(self, ctx: abc.SlashContext, /) -> bool:
        # <<inherited docstring from tanjun.abc.SlashCommand>>.
        ctx.set_command(self)
        result = await utilities.gather_checks(ctx, self._checks)
        ctx.set_command(None)
        return result

    def copy(
        self: _BaseSlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _BaseSlashCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._parent = parent
            return super().copy(_new=_new)

        return super().copy(_new=_new)

    def load_into_component(self, component: abc.Component, /) -> None:
        # <<inherited docstring from tanjun.components.load_into_component>>.
        if not self._parent:
            component.add_slash_command(self)


class SlashCommandGroup(BaseSlashCommand, abc.SlashCommandGroup):
    """Standard implementation of a slash command group.

    .. note::
        Unlike message command grups, slash command groups cannot
        be callable functions themselves.
    """

    __slots__ = ("_commands", "_default_permission")

    def __init__(
        self,
        name: str,
        description: str,
        /,
        *,
        default_to_ephemeral: typing.Optional[bool] = None,
        default_permission: bool = True,
        is_global: bool = True,
    ) -> None:
        r"""Initialise a slash command group.

        .. note::
            Under the standard implementation, `is_global` is used to determine
            whether the command should be bulk set by `tanjun.Client.set_global_commands`
            or when `set_global_commands` is True

        Parameters
        ----------
        name : str
            The name of the command group.

            This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase.
        description : str
            The description of the command group.

        Other Parameters
        ----------------
        default_permission : bool
            Whether this command can be accessed without set permissions.

            Defaults to `True`, meaning that users can access the command by default.
        default_to_ephemeral : typing.Optional[bool]
            Whether this command's responses should default to ephemeral unless flags
            are set to override this.

            If this is left as `None` then the default set on the parent command(s),
            component or client will be in effect.
        is_global : bool
            Whether this command is a global command. Defaults to `True`.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the command name has uppercase characters.
            * If the description is over 100 characters long.
        """
        super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global)
        self._commands: dict[str, abc.BaseSlashCommand] = {}
        self._default_permission = default_permission

    @property
    def commands(self) -> collections.Collection[abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.SlashCommandGroup>>.
        return self._commands.copy().values()

    def build(self) -> special_endpoints_api.CommandBuilder:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        builder = _CommandBuilder(self._name, self._description, False).set_default_permission(self._default_permission)
        for command in self._commands.values():
            option_type = (
                hikari.OptionType.SUB_COMMAND_GROUP
                if isinstance(command, abc.SlashCommandGroup)
                else hikari.OptionType.SUB_COMMAND
            )
            command_builder = command.build()
            builder.add_option(
                hikari.CommandOption(
                    type=option_type,
                    name=command.name,
                    description=command_builder.description,
                    is_required=False,
                    options=command_builder.options,
                )
            )

        return builder

    def copy(
        self: _SlashCommandGroupT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _SlashCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._commands = {name: command.copy() for name, command in self._commands.items()}
            return super().copy(_new=_new, parent=parent)

        return super().copy(_new=_new, parent=parent)

    def add_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT:
        """Add a slash command to this group.

        .. warning::
            Command groups are only supported within top-level groups.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to add to this group.

        Returns
        -------
        Self
            Object of this group to enable chained calls.
        """
        if self._parent and isinstance(command, abc.SlashCommandGroup):
            raise ValueError("Cannot add a slash command group to a nested slash command group")

        if len(self._commands) == 25:
            raise ValueError("Cannot add more than 25 commands to a slash command group")

        if command.name in self._commands:
            raise ValueError(f"Command with name {command.name!r} already exists in this group")

        command.set_parent(self)
        self._commands[command.name] = command
        return self

    def remove_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT:
        """Remove a command from this group.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to remove from this group.

        Returns
        -------
        Self
            Object of this group to enable chained calls.
        """
        del self._commands[command.name]
        return self

    def with_command(self, command: abc.BaseSlashCommandT, /) -> abc.BaseSlashCommandT:
        """Add a slash command to this group through a decorator call.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to add to this group.

        Returns
        -------
        tanjun.abc.BaseSlashCommand
            Command which was added to this group.
        """
        self.add_command(command)
        return command

    async def execute(
        self,
        ctx: abc.SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        if not option and ctx.interaction.options:
            option = ctx.interaction.options[0]

        elif option and option.options:
            option = option.options[0]

        else:
            raise RuntimeError("Missing sub-command option")

        if command := self._commands.get(option.name):
            if command.defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(command.defaults_to_ephemeral)

            if await command.check_context(ctx):
                await command.execute(ctx, option=option, hooks=hooks)
                return

        await ctx.mark_not_found()


class SlashCommand(BaseSlashCommand, abc.SlashCommand[abc.CommandCallbackSigT]):
    """Standard implementation of a slash command."""

    __slots__ = ("_always_defer", "_builder", "_callback", "_client", "_tracked_options", "_wrapped_command")

    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        description: str,
        /,
        *,
        always_defer: bool = False,
        default_permission: bool = True,
        default_to_ephemeral: typing.Optional[bool] = None,
        is_global: bool = True,
        sort_options: bool = True,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        r"""Initialise a slash command.

        .. note::
            Under the standard implementation, `is_global` is used to determine whether
            the command should be bulk set by `tanjun.Client.set_global_commands`
            or when `set_global_commands` is True

        .. warning::
            `default_permission` and `is_global` are ignored for commands within
            slash command groups.

        Parameters
        ----------
        callback : collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]]
            Callback to execute when the command is invoked.

            This should be an asynchronous callback which takes one positional
            argument of type `tanjun.abc.SlashContext`, returns `None` and may use
            dependency injection to access other services.
        name : str
            The command's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The command's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        always_defer : bool
            Whether the contexts this command is executed with should always be deferred
            before being passed to the command's callback.

            Defaults to `False`.

            .. note::
                The ephemeral state of the first response is decided by whether the
                deferral is ephemeral.
        default_permission : bool
            Whether this command can be accessed without set permissions.

            Defaults to `True`, meaning that users can access the command by default.
        default_to_ephemeral : typing.Optional[bool]
            Whether this command's responses should default to ephemeral unless flags
            are set to override this.

            If this is left as `None` then the default set on the parent command(s),
            component or client will be in effect.
        is_global : bool
            Whether this command is a global command. Defaults to `True`.
        sort_options : bool
            Whether this command should sort its set options based on whether
            they're required.

            If this is `True` then the options are re-sorted to meet the requirement
            from Discord that required command options be listed before optional
            ones.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the command name has uppercase characters.
            * If the description is over 100 characters long.
        """
        super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global)
        if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)):
            callback = typing.cast(abc.CommandCallbackSigT, callback.callback)

        self._always_defer = always_defer
        self._builder = _CommandBuilder(name, description, sort_options).set_default_permission(default_permission)
        self._callback = injecting.CallbackDescriptor[None](callback)
        self._client: typing.Optional[abc.Client] = None
        self._tracked_options: dict[str, _TrackedOption] = {}
        self._wrapped_command = _wrapped_command

    if typing.TYPE_CHECKING:
        __call__: abc.CommandCallbackSigT

    else:

        async def __call__(self, *args, **kwargs) -> None:
            await self._callback.callback(*args, **kwargs)

    @property
    def callback(self) -> abc.CommandCallbackSigT:
        # <<inherited docstring from tanjun.abc.SlashCommand>>.
        return typing.cast(abc.CommandCallbackSigT, self._callback.callback)

    @property
    def needs_injector(self) -> bool:
        return (
            self._callback.needs_injector
            or any(option.needs_injector for option in self._tracked_options.values())
            or super().needs_injector
        )

    def bind_client(self: _SlashCommandT, client: abc.Client, /) -> _SlashCommandT:
        self._client = client
        super().bind_client(client)
        for option in self._tracked_options.values():
            option.check_client(client)

        return self

    def build(self) -> special_endpoints_api.CommandBuilder:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._builder.sort().copy()

    def load_into_component(self, component: abc.Component, /) -> None:
        super().load_into_component(component)
        if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader):
            self._wrapped_command.load_into_component(component)

    def _add_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        type_: typing.Union[hikari.OptionType, int] = hikari.OptionType.STRING,
        /,
        *,
        always_float: bool = False,
        channel_types: typing.Optional[collections.Sequence[int]] = None,
        choices: typing.Union[
            collections.Mapping[str, typing.Union[str, int, float]], collections.Sequence[typing.Any], None
        ] = None,
        converters: typing.Union[collections.Iterable[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Union[int, float, None] = None,
        max_value: typing.Union[int, float, None] = None,
        only_member: bool = False,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        _validate_name(name)
        if len(description) > 100:
            raise ValueError("The option description cannot be over 100 characters in length")

        if len(self._builder.options) == 25:
            raise ValueError("Slash commands cannot have more than 25 options")

        if min_value and max_value and min_value > max_value:
            raise ValueError("The min value cannot be greater than the max value")

        type_ = hikari.OptionType(type_)
        if isinstance(converters, collections.Iterable):
            converters_ = list(map(_convert_to_injectable, converters))

        else:
            converters_ = [_convert_to_injectable(converters)]

        if self._client:
            for converter in converters_:
                if isinstance(converter.callback, conversion.BaseConverter):
                    converter.callback.check_client(self._client, f"{self._name}'s slash option '{name}'")

        if choices is None:
            actual_choices: typing.Optional[list[hikari.CommandChoice]] = None

        elif isinstance(choices, collections.Mapping):
            actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices.items()]

        else:
            warnings.warn(
                "Passing a sequence of tuples to `choices` is deprecated since 2.1.2a1, "
                "please pass a mapping instead.",
                category=DeprecationWarning,
                stacklevel=2 + _stack_level,
            )
            actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices]

        if actual_choices and len(actual_choices) > 25:
            raise ValueError("Slash command options cannot have more than 25 choices")

        required = default is _UNDEFINED_DEFAULT
        self._builder.add_option(
            hikari.CommandOption(
                type=type_,
                name=name,
                description=description,
                is_required=required,
                choices=actual_choices,
                channel_types=channel_types,
                min_value=min_value,
                max_value=max_value,
            )
        )
        if pass_as_kwarg:
            self._tracked_options[name] = _TrackedOption(
                name=name,
                option_type=type_,
                always_float=always_float,
                converters=converters_,
                default=default,
                only_member=only_member,
            )
        return self

    def add_str_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None,
        converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add a string option to the slash command.

        .. note::
            As a shorthand, `choices` also supports passing a list of strings
            rather than a dict of names to values (each string will used as
            both the choice's name and value with the names being capitalised).

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        choices : typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None]
            The option's choices.

            This either a mapping of [option_name, option_value] where both option_name
            and option_value should be strings of up to 100 characters or a sequence
            of strings where the string will be used for both the choice's name and
            value.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the `coverters` field will be ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
        """
        if choices is None:
            actual_choices = None

        elif isinstance(choices, collections.Mapping):
            actual_choices = choices

        else:
            actual_choices = {}
            warned = False
            for choice in choices:
                if isinstance(choice, tuple):  # type: ignore[unreachable]  # the point of this is for deprecation
                    if not warned:  # type: ignore[unreachable]  # mypy sees `warned = True` and messes up.
                        warnings.warn(
                            "Passing a sequence of tuples for 'choices' is deprecated since 2.1.2a1, "
                            "please pass a mapping instead.",
                            category=DeprecationWarning,
                            stacklevel=2 + _stack_level,
                        )
                        warned = True

                    actual_choices[choice[0]] = choice[1]

                else:
                    actual_choices[choice.capitalize()] = choice

        return self._add_option(
            name,
            description,
            hikari.OptionType.STRING,
            choices=actual_choices,
            converters=converters,
            default=default,
            pass_as_kwarg=pass_as_kwarg,
        )

    def add_int_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        choices: typing.Optional[collections.Mapping[str, int]] = None,
        converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Optional[int] = None,
        max_value: typing.Optional[int] = None,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add an integer option to the slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        choices : typing.Optional[collections.abc.Mapping[str, int]]
            The option's choices.

            This is a mapping of [option_name, option_value] where option_name
            should be a string of up to 100 characters and option_value should
            be an integer.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        min_value : typing.Optional[int]
            The option's (inclusive) minimum value.

            Defaults to no minimum value.
        max_value : typing.Optional[int]
            The option's (inclusive) maximum value.

            Defaults to no minimum value.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the `coverters` field will be ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
            * If `min_value` is greater than `max_value`.
        """
        return self._add_option(
            name,
            description,
            hikari.OptionType.INTEGER,
            choices=choices,
            converters=converters,
            default=default,
            min_value=min_value,
            max_value=max_value,
            pass_as_kwarg=pass_as_kwarg,
            _stack_level=_stack_level + 1,
        )

    def add_float_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        always_float: bool = True,
        choices: typing.Optional[collections.Mapping[str, float]] = None,
        converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Optional[float] = None,
        max_value: typing.Optional[float] = None,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add a float option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        always_float : bool
            If this is set to `True` then the value will always be converted to a
            float (this will happen before it's passed to converters).

            This masks behaviour from Discord where we will either be provided a `float`
            or `int` dependent on what the user provided and defaults to `True`.
        choices : typing.Optional[collections.abc.Mapping[str, float]]
            The option's choices.

            This is a mapping of [option_name, option_value] where option_name
            should be a string of up to 100 characters and option_value should
            be a float.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        min_value : typing.Optional[float]
            The option's (inclusive) minimum value.

            Defaults to no minimum value.
        max_value : typing.Optional[float]
            The option's (inclusive) maximum value.

            Defaults to no minimum value.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the fields `coverters`, and `always_float` will be
            ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
            * If `min_value` is greater than `max_value`.
        """
        return self._add_option(
            name,
            description,
            hikari.OptionType.FLOAT,
            choices=choices,
            converters=converters,
            default=default,
            min_value=float(min_value) if min_value is not None else None,
            max_value=float(max_value) if max_value is not None else None,
            pass_as_kwarg=pass_as_kwarg,
            always_float=always_float,
            _stack_level=_stack_level + 1,
        )

    def add_bool_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a boolean option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(
            name, description, hikari.OptionType.BOOLEAN, default=default, pass_as_kwarg=pass_as_kwarg
        )

    def add_user_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a user option to a slash command.

        .. note::
            This may result in `hikari.InteractionMember` or
            `hikari.users.User` if the user isn't in the current guild or if this
            command was executed in a DM channel.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.USER, default=default, pass_as_kwarg=pass_as_kwarg)

    def add_member_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
    ) -> _SlashCommandT:
        r"""Add a member option to a slash command.

        .. note::
            This will always result in `hikari.InteractionMember`.

        .. warning::
            Unlike the other options, this is an artificial option which adds
            a restraint to the USER option type and therefore cannot have
            `pass_as_kwarg` set to `False` as this artificial constaint isn't
            present when its not being passed as a keyword argument.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.USER, default=default, only_member=True)

    def add_channel_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        types: typing.Optional[collections.Collection[type[hikari.PartialChannel]]] = None,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a channel option to a slash command.

        .. note::
            This will always result in `hikari.InteractionChannel`.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Parameters
        ----------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        types : typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]]
            A collection of the channel classes this option should accept.

            If left as `None` or empty then the option will allow all channel types.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
            * If an invalid type is passed in `types`.
        """
        import itertools

        if types:
            try:
                channel_types = list(set(itertools.chain.from_iterable(map(_channel_types.__getitem__, types))))

            except KeyError as exc:
                raise ValueError(f"Unknown channel type {exc.args[0]}") from exc

        else:
            channel_types = None

        return self._add_option(
            name,
            description,
            hikari.OptionType.CHANNEL,
            channel_types=channel_types,
            default=default,
            pass_as_kwarg=pass_as_kwarg,
        )

    def add_role_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a role option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.ROLE, default=default, pass_as_kwarg=pass_as_kwarg)

    def add_mentionable_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a mentionable option to a slash command.

        .. note::
            This may target roles, guild members or users and results in
            `Union[hikari.User, hikari.InteractionMember, hikari.Role]`.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(
            name, description, hikari.OptionType.MENTIONABLE, default=default, pass_as_kwarg=pass_as_kwarg
        )

    async def _process_args(self, ctx: abc.SlashContext, /) -> collections.Mapping[str, typing.Any]:
        keyword_args: dict[str, typing.Union[int, float, str, hikari.User, hikari.Role, hikari.InteractionChannel]] = {}
        for tracked_option in self._tracked_options.values():
            if not (option := ctx.options.get(tracked_option.name)):
                if tracked_option.default is _UNDEFINED_DEFAULT:
                    raise RuntimeError(  # TODO: ConversionError?
                        f"Required option {tracked_option.name} is missing data, are you sure your commands"
                        " are up to date?"
                    )

                else:
                    keyword_args[tracked_option.name] = tracked_option.default

            elif option.type is hikari.OptionType.USER:
                member: typing.Optional[hikari.InteractionMember] = None
                if tracked_option.is_only_member and not (member := option.resolve_to_member(default=None)):
                    raise errors.ConversionError(
                        f"Couldn't find member for provided user: {option.value}", tracked_option.name
                    )

                keyword_args[option.name] = member or option.resolve_to_user()

            elif option.type is hikari.OptionType.CHANNEL:
                keyword_args[option.name] = option.resolve_to_channel()

            elif option.type is hikari.OptionType.ROLE:
                keyword_args[option.name] = option.resolve_to_role()

            elif option.type is hikari.OptionType.MENTIONABLE:
                keyword_args[option.name] = option.resolve_to_mentionable()

            else:
                value = option.value
                # To be type safe we obfuscate the fact that discord's double type will provide an int or float
                # depending on the value Discord inputs by always casting to float.
                if tracked_option.type is hikari.OptionType.FLOAT and tracked_option.is_always_float:
                    value = float(value)

                if tracked_option.converters:
                    value = await tracked_option.convert(ctx, option.value)

                keyword_args[option.name] = value

        return keyword_args

    async def execute(
        self,
        ctx: abc.SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        if self._always_defer and not ctx.has_been_deferred and not ctx.has_responded:
            await ctx.defer()

        ctx = ctx.set_command(self)
        own_hooks = self._hooks or _EMPTY_HOOKS
        try:
            await own_hooks.trigger_pre_execution(ctx, hooks=hooks)

            if self._tracked_options:
                kwargs = await self._process_args(ctx)

            else:
                kwargs = _EMPTY_DICT

            await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)

        except errors.CommandError as exc:
            await ctx.respond(exc.message)

        except errors.HaltExecution:
            # Unlike a message command, this won't necessarily reach the client level try except
            # block so we have to handle this here.
            await ctx.mark_not_found()

        except Exception as exc:
            if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0:
                raise

        else:
            await own_hooks.trigger_success(ctx, hooks=hooks)

        finally:
            await own_hooks.trigger_post_execution(ctx, hooks=hooks)

    def copy(
        self: _SlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _SlashCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._callback = copy.copy(self._callback)
            return super().copy(_new=_new, parent=parent)

        return super().copy(_new=_new, parent=parent)


def as_message_command(
    name: str, /, *names: str
) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], MessageCommand[abc.CommandCallbackSigT]]:
    """Build a message command from a decorated callback.

    Parameters
    ----------
    name : str
        The command name.

    Other Parameters
    ----------------
    *names : str
        Variable positional arguments of other names for the command.

    Returns
    -------
    collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]
        The decorator callback used to make a `MessageCommand`.

        This can either wrap a raw command callback or another callable command instance
        (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage
        loading the other command into a component when using `tanjun.Component.load_from_scope`.
    """

    def decorator(
        callback: _CallbackishT[abc.CommandCallbackSigT],
        /,
    ) -> MessageCommand[abc.CommandCallbackSigT]:
        if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)):
            return MessageCommand(callback.callback, name, *names, _wrapped_command=callback)

        return MessageCommand(callback, name, *names)

    return decorator


def as_message_command_group(
    name: str, /, *names: str, strict: bool = False
) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT]], MessageCommandGroup[abc.CommandCallbackSigT]]:
    """Build a message command group from a decorated callback.

    Parameters
    ----------
    name : str
        The command name.

    Other Parameters
    ----------------
    *names : str
        Variable positional arguments of other names for the command.
    strict : bool
        Whether this command group should only allow commands without spaces in their names.

        This allows for a more optimised command search pattern to be used and
        enforces that command names are unique to a single command within the group.

    Returns
    -------
    collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]
        The decorator callback used to make a `MessageCommandGroup`.

        This can either wrap a raw command callback or another callable command instance
        (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage
        loading the other command into a component when using `tanjun.Component.load_from_scope`.
    """

    def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> MessageCommandGroup[abc.CommandCallbackSigT]:
        if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)):
            return MessageCommandGroup(callback.callback, name, *names, strict=strict, _wrapped_command=callback)

        return MessageCommandGroup(callback, name, *names, strict=strict)

    return decorator


class MessageCommand(PartialCommand[abc.MessageContext], abc.MessageCommand[abc.CommandCallbackSigT]):
    """Standard implementation of a message command."""

    __slots__ = ("_callback", "_names", "_parent", "_parser", "_wrapped_command")

    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        /,
        *names: str,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        """Initialise a message command.

        Parameters
        ----------
        callback : collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]]
            Callback to execute when the command is invoked.

            This should be an asynchronous callback which takes one positional
            argument of type `tanjun.abc.MessageContext`, returns `None` and may use
            dependency injection to access other services.
        name : str
            The command name.

        Other Parameters
        ----------------
        *names : str
            Variable positional arguments of other names for the command.
        """
        super().__init__()
        if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)):
            callback = typing.cast(abc.CommandCallbackSigT, callback.callback)

        self._callback = injecting.CallbackDescriptor[None](callback)
        self._names = list(dict.fromkeys((name, *names)))
        self._parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None
        self._parser: typing.Optional[abc.MessageParser] = None
        self._wrapped_command = _wrapped_command

    def __repr__(self) -> str:
        return f"Command <{self._names}>"

    if typing.TYPE_CHECKING:
        __call__: abc.CommandCallbackSigT

    else:

        async def __call__(self, *args, **kwargs) -> None:
            await self._callback.callback(*args, **kwargs)

    @property
    def callback(self) -> abc.CommandCallbackSigT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        return typing.cast(abc.CommandCallbackSigT, self._callback.callback)

    @property
    # <<inherited docstring from tanjun.abc.MessageCommand>>.
    def names(self) -> collections.Collection[str]:
        return self._names.copy()

    @property
    def needs_injector(self) -> bool:
        return self._callback.needs_injector

    @property
    def parent(self) -> typing.Optional[abc.MessageCommandGroup[typing.Any]]:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        return self._parent

    @property
    def parser(self) -> typing.Optional[abc.MessageParser]:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        return self._parser

    def bind_client(self: _MessageCommandT, client: abc.Client, /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_client(client)
        if self._parser:
            self._parser.bind_client(client)

        return self

    def bind_component(self: _MessageCommandT, component: abc.Component, /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_component(component)
        if self._parser:
            self._parser.bind_component(component)

        return self

    def copy(
        self: _MessageCommandT,
        *,
        parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None,
        _new: bool = True,
    ) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if not _new:
            self._callback = copy.copy(self._callback)
            self._names = self._names.copy()
            self._parent = parent
            self._parser = self._parser.copy() if self._parser else None
            return super().copy(_new=_new)

        return super().copy(_new=_new)

    def set_parent(
        self: _MessageCommandT, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]], /
    ) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        self._parent = parent
        return self

    def set_parser(self: _MessageCommandT, parser: typing.Optional[abc.MessageParser], /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        self._parser = parser
        return self

    async def check_context(self, ctx: abc.MessageContext, /) -> bool:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        ctx.set_command(self)
        result = await utilities.gather_checks(ctx, self._checks)
        ctx.set_command(None)
        return result

    async def execute(
        self,
        ctx: abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        ctx = ctx.set_command(self)
        own_hooks = self._hooks or _EMPTY_HOOKS
        try:
            await own_hooks.trigger_pre_execution(ctx, hooks=hooks)

            if self._parser is not None:
                kwargs = await self._parser.parse(ctx)

            else:
                kwargs = _EMPTY_DICT

            await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)

        except errors.CommandError as exc:
            response = exc.message if len(exc.message) <= 2000 else exc.message[:1997] + "..."
            await ctx.respond(content=response)

        except errors.HaltExecution:
            raise

        except Exception as exc:
            if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0:
                raise

        else:
            # TODO: how should this be handled around CommandError?
            await own_hooks.trigger_success(ctx, hooks=hooks)

        finally:
            await own_hooks.trigger_post_execution(ctx, hooks=hooks)

    def load_into_component(self, component: abc.Component, /) -> None:
        # <<inherited docstring from tanjun.components.load_into_component>>.
        if not self._parent:
            component.add_message_command(self)

        if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader):
            self._wrapped_command.load_into_component(component)


class MessageCommandGroup(MessageCommand[abc.CommandCallbackSigT], abc.MessageCommandGroup[abc.CommandCallbackSigT]):
    """Standard implementation of a message command group."""

    __slots__ = ("_commands", "_is_strict", "_names_to_commands")

    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        /,
        *names: str,
        strict: bool = False,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        """Initialise a message command group.

        Parameters
        ----------
        name : str
            The command name.

        Other Parameters
        ----------------
        *names : str
            Variable positional arguments of other names for the command.
        strict : bool
            Whether this command group should only allow commands without spaces in their names.

            This allows for a more optimised command search pattern to be used and
            enforces that command names are unique to a single command within the group.
        """
        super().__init__(callback, name, *names, _wrapped_command=_wrapped_command)
        self._commands: list[abc.MessageCommand[typing.Any]] = []
        self._is_strict = strict
        self._names_to_commands: dict[str, abc.MessageCommand[typing.Any]] = {}

    def __repr__(self) -> str:
        return f"CommandGroup <{len(self._commands)}: {self._names}>"

    @property
    def commands(self) -> collections.Collection[abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.MessageCommandGroup>>.
        return self._commands.copy()

    @property
    def is_strict(self) -> bool:
        return self._is_strict

    def copy(
        self: _MessageCommandGroupT,
        *,
        parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None,
        _new: bool = True,
    ) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if not _new:
            commands = {command: command.copy(parent=self) for command in self._commands}
            self._commands = list(commands.values())
            self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()}
            return super().copy(parent=parent, _new=_new)

        return super().copy(parent=parent, _new=_new)

    def add_command(self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /) -> _MessageCommandGroupT:
        """Add a command to this group.

        Parameters
        ----------
        command : MessageCommand
            The command to add.

        Returns
        -------
        Self
            The group instance to enable chained calls.

        Raises
        ------
        ValueError
            If one of the command's names is already registered in a strict
            command group.
        """
        if command in self._commands:
            return self

        if self._is_strict:
            if any(" " in name for name in command.names):
                raise ValueError("Sub-command names may not contain spaces in a strict message command group")

            if name_conflicts := self._names_to_commands.keys() & command.names:
                raise ValueError(
                    "Sub-command names must be unique in a strict message command group. "
                    "The following conflicts were found " + ", ".join(name_conflicts)
                )

            self._names_to_commands.update((name, command) for name in command.names)

        command.set_parent(self)
        self._commands.append(command)
        return self

    def remove_command(
        self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /
    ) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.MessageCommandGroup>>.
        self._commands.remove(command)
        if self._is_strict:
            for name in command.names:
                if self._names_to_commands.get(name) == command:
                    del self._names_to_commands[name]

        command.set_parent(None)
        return self

    def with_command(self, command: AnyMessageCommandT, /) -> AnyMessageCommandT:
        self.add_command(command)
        return command

    def bind_client(self: _MessageCommandGroupT, client: abc.Client, /) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_client(client)
        for command in self._commands:
            command.bind_client(client)

        return self

    def bind_component(self: _MessageCommandGroupT, component: abc.Component, /) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_component(component)
        for command in self._commands:
            command.bind_component(component)

        return self

    def find_command(self, content: str, /) -> collections.Iterable[tuple[str, abc.MessageCommand[typing.Any]]]:
        if self._is_strict:
            name = content.split(" ")[0]
            if command := self._names_to_commands.get(name):
                yield name, command
            return

        for command in self._commands:
            if (name_ := utilities.match_prefix_names(content, command.names)) is not None:
                yield name_, command

    async def execute(
        self,
        ctx: abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if ctx.message.content is None:
            raise ValueError("Cannot execute a command with a content-less message")

        if self._hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._hooks)

        for name, command in self.find_command(ctx.content):
            if await command.check_context(ctx):
                content = ctx.content[len(name) :]
                lstripped_content = content.lstrip()
                space_len = len(content) - len(lstripped_content)
                ctx.set_triggering_name(ctx.triggering_name + (" " * space_len) + name)
                ctx.set_content(lstripped_content)
                await command.execute(ctx, hooks=hooks)
                return

        await super().execute(ctx, hooks=hooks)

Standard implementation of Tanjun's command objects.

#   def as_message_command( name: str, /, *names: str ) -> collections.abc.Callable[[typing.Union[~CommandCallbackSigT, tanjun.abc.MessageCommand[~CommandCallbackSigT], tanjun.abc.SlashCommand[~CommandCallbackSigT]]], tanjun.commands.MessageCommand[~CommandCallbackSigT]]:
View Source
def as_message_command(
    name: str, /, *names: str
) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], MessageCommand[abc.CommandCallbackSigT]]:
    """Build a message command from a decorated callback.

    Parameters
    ----------
    name : str
        The command name.

    Other Parameters
    ----------------
    *names : str
        Variable positional arguments of other names for the command.

    Returns
    -------
    collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]
        The decorator callback used to make a `MessageCommand`.

        This can either wrap a raw command callback or another callable command instance
        (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage
        loading the other command into a component when using `tanjun.Component.load_from_scope`.
    """

    def decorator(
        callback: _CallbackishT[abc.CommandCallbackSigT],
        /,
    ) -> MessageCommand[abc.CommandCallbackSigT]:
        if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)):
            return MessageCommand(callback.callback, name, *names, _wrapped_command=callback)

        return MessageCommand(callback, name, *names)

    return decorator

Build a message command from a decorated callback.

Parameters
  • name (str): The command name.
Other Parameters
  • *names (str): Variable positional arguments of other names for the command.
Returns
  • collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]: The decorator callback used to make a MessageCommand.

This can either wrap a raw command callback or another callable command instance (e.g. SlashCommand, MessageCommand, MessageCommandGroup) and will manage loading the other command into a component when using tanjun.Component.load_from_scope.

#   def as_message_command_group( name: str, /, *names: str, strict: bool = False ) -> collections.abc.Callable[[typing.Union[~CommandCallbackSigT, tanjun.abc.MessageCommand[~CommandCallbackSigT], tanjun.abc.SlashCommand[~CommandCallbackSigT]]], tanjun.commands.MessageCommandGroup[~CommandCallbackSigT]]:
View Source
def as_message_command_group(
    name: str, /, *names: str, strict: bool = False
) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT]], MessageCommandGroup[abc.CommandCallbackSigT]]:
    """Build a message command group from a decorated callback.

    Parameters
    ----------
    name : str
        The command name.

    Other Parameters
    ----------------
    *names : str
        Variable positional arguments of other names for the command.
    strict : bool
        Whether this command group should only allow commands without spaces in their names.

        This allows for a more optimised command search pattern to be used and
        enforces that command names are unique to a single command within the group.

    Returns
    -------
    collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]
        The decorator callback used to make a `MessageCommandGroup`.

        This can either wrap a raw command callback or another callable command instance
        (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage
        loading the other command into a component when using `tanjun.Component.load_from_scope`.
    """

    def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> MessageCommandGroup[abc.CommandCallbackSigT]:
        if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)):
            return MessageCommandGroup(callback.callback, name, *names, strict=strict, _wrapped_command=callback)

        return MessageCommandGroup(callback, name, *names, strict=strict)

    return decorator

Build a message command group from a decorated callback.

Parameters
  • name (str): The command name.
Other Parameters
  • *names (str): Variable positional arguments of other names for the command.
  • strict (bool): Whether this command group should only allow commands without spaces in their names.

    This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group.

Returns
  • collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]: The decorator callback used to make a MessageCommandGroup.

This can either wrap a raw command callback or another callable command instance (e.g. SlashCommand, MessageCommand, MessageCommandGroup) and will manage loading the other command into a component when using tanjun.Component.load_from_scope.

#   def as_slash_command( name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: Optional[bool] = None, is_global: bool = True, sort_options: bool = True ) -> collections.abc.Callable[[typing.Union[~CommandCallbackSigT, tanjun.abc.MessageCommand[~CommandCallbackSigT], tanjun.abc.SlashCommand[~CommandCallbackSigT]]], tanjun.commands.SlashCommand[~CommandCallbackSigT]]:
View Source
def as_slash_command(
    name: str,
    description: str,
    /,
    *,
    always_defer: bool = False,
    default_permission: bool = True,
    default_to_ephemeral: typing.Optional[bool] = None,
    is_global: bool = True,
    sort_options: bool = True,
) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], SlashCommand[abc.CommandCallbackSigT]]:
    r"""Build a `SlashCommand` by decorating a function.

    .. note::
        Under the standard implementation, `is_global` is used to determine whether
        the command should be bulk set by `tanjun.Client.set_global_commands`
        or when `set_global_commands` is True

    .. warning::
        `default_permission` and `is_global` are ignored for commands within
        slash command groups.

    Examples
    --------
    ```py
    @as_slash_command("ping", "Get the bot's latency")
    async def ping_command(self, ctx: tanjun.abc.SlashContext) -> None:
        start_time = time.perf_counter()
        await ctx.rest.fetch_my_user()
        time_taken = (time.perf_counter() - start_time) * 1_000
        await ctx.respond(f"PONG\n - REST: {time_taken:.0f}mss")
    ```

    Parameters
    ----------
    name : str
        The command's name.

        This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
    description : str
        The command's description.
        This should be inclusively between 1-100 characters in length.

    Other Parameters
    ----------------
    always_defer : bool
        Whether the contexts this command is executed with should always be deferred
        before being passed to the command's callback.

        Defaults to `False`.

        .. note::
            The ephemeral state of the first response is decided by whether the
            deferral is ephemeral.
    default_permission : bool
        Whether this command can be accessed without set permissions.

        Defaults to `True`, meaning that users can access the command by default.
    default_to_ephemeral : typing.Optional[bool]
        Whether this command's responses should default to ephemeral unless flags
        are set to override this.

        If this is left as `None` then the default set on the parent command(s),
        component or client will be in effect.
    is_global : bool
        Whether this command is a global command. Defaults to `True`.
    sort_options : bool
        Whether this command should sort its set options based on whether
        they're required.

        If this is `True` then the options are re-sorted to meet the requirement
        from Discord that required command options be listed before optional
        ones.

    Returns
    -------
    collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], SlashCommand[CommandCallbackSigT]]
        The decorator callback used to make a `SlashCommand`.

        This can either wrap a raw command callback or another callable command instance
        (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage
        loading the other command into a component when using `tanjun.Component.load_from_scope`.

    Raises
    ------
    ValueError
        Raises a value error for any of the following reasons:
        * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
        * If the command name has uppercase characters.
        * If the description is over 100 characters long.
    """

    def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> SlashCommand[abc.CommandCallbackSigT]:
        if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)):
            return SlashCommand(
                callback.callback,
                name,
                description,
                always_defer=always_defer,
                default_permission=default_permission,
                default_to_ephemeral=default_to_ephemeral,
                is_global=is_global,
                sort_options=sort_options,
                _wrapped_command=callback,
            )

        return SlashCommand(
            callback,
            name,
            description,
            always_defer=always_defer,
            default_permission=default_permission,
            default_to_ephemeral=default_to_ephemeral,
            is_global=is_global,
            sort_options=sort_options,
        )

    return decorator

Build a SlashCommand by decorating a function.

Note: Under the standard implementation, is_global is used to determine whether the command should be bulk set by tanjun.Client.set_global_commands or when set_global_commands is True

Warning: default_permission and is_global are ignored for commands within slash command groups.

Examples
@as_slash_command("ping", "Get the bot's latency")
async def ping_command(self, ctx: tanjun.abc.SlashContext) -> None:
    start_time = time.perf_counter()
    await ctx.rest.fetch_my_user()
    time_taken = (time.perf_counter() - start_time) * 1_000
    await ctx.respond(f"PONG\n - REST: {time_taken:.0f}mss")
Parameters
  • name (str): The command's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The command's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • always_defer (bool): Whether the contexts this command is executed with should always be deferred before being passed to the command's callback.

    Defaults to False.

    Note: The ephemeral state of the first response is decided by whether the deferral is ephemeral.

  • default_permission (bool): Whether this command can be accessed without set permissions.

    Defaults to True, meaning that users can access the command by default.

  • default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.

    If this is left as None then the default set on the parent command(s), component or client will be in effect.

  • is_global (bool): Whether this command is a global command. Defaults to True.
  • sort_options (bool): Whether this command should sort its set options based on whether they're required.

    If this is True then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones.

Returns
  • collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], SlashCommand[CommandCallbackSigT]]: The decorator callback used to make a SlashCommand.

This can either wrap a raw command callback or another callable command instance (e.g. SlashCommand, MessageCommand, MessageCommandGroup) and will manage loading the other command into a component when using tanjun.Component.load_from_scope.

Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the command name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the command name has uppercase characters.
    • If the description is over 100 characters long.
#   def slash_command_group( name: str, description: str, /, *, default_permission: bool = True, default_to_ephemeral: Optional[bool] = None, is_global: bool = True ) -> tanjun.commands.SlashCommandGroup:
View Source
def slash_command_group(
    name: str,
    description: str,
    /,
    *,
    default_permission: bool = True,
    default_to_ephemeral: typing.Optional[bool] = None,
    is_global: bool = True,
) -> SlashCommandGroup:
    r"""Create a slash command group.

    Examples
    --------
    Sub-commands can be added to the created slash command object through
    the following decorator based approach:
    ```python
    help_group = tanjun.slash_command_group("help", "get help")

    @help_group.with_command
    @tanjun.with_str_slash_option("command_name", "command name")
    @tanjun.as_slash_command("command", "Get help with a command")
    async def help_command_command(ctx: tanjun.abc.SlashContext, command_name: str) -> None:
        ...

    @help_group.with_command
    @tanjun.as_slash_command("me", "help me")
    async def help_me_command(ctx: tanjun.abc.SlashContext) -> None:
        ...

    component = tanjun.Component().add_slash_command(help_group)
    ```

    Notes
    -----
    * Unlike message command grups, slash command groups cannot
      be callable functions themselves.
    * Under the standard implementation, `is_global` is used to determine whether
      the command should be bulk set by `tanjun.Client.set_global_commands`
      or when `set_global_commands` is True

    Parameters
    ----------
    name : str
        The name of the command group.

        This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase.
    description : str
        The description of the command group.

    Other Parameters
    ----------------
    default_permission : bool
        Whether this command can be accessed without set permissions.

        Defaults to `True`, meaning that users can access the command by default.
    default_to_ephemeral : typing.Optional[bool]
        Whether this command's responses should default to ephemeral unless flags
        are set to override this.

        If this is left as `None` then the default set on the parent command(s),
        component or client will be in effect.
    is_global : bool
        Whether this command is a global command. Defaults to `True`.

    Returns
    -------
    SlashCommandGroup
        The command group.

    Raises
    ------
    ValueError
        Raises a value error for any of the following reasons:
        * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
        * If the command name has uppercase characters.
        * If the description is over 100 characters long.
    """
    return SlashCommandGroup(
        name,
        description,
        default_permission=default_permission,
        default_to_ephemeral=default_to_ephemeral,
        is_global=is_global,
    )

Create a slash command group.

Examples

Sub-commands can be added to the created slash command object through the following decorator based approach:

help_group = tanjun.slash_command_group("help", "get help")

@help_group.with_command
@tanjun.with_str_slash_option("command_name", "command name")
@tanjun.as_slash_command("command", "Get help with a command")
async def help_command_command(ctx: tanjun.abc.SlashContext, command_name: str) -> None:
    ...

@help_group.with_command
@tanjun.as_slash_command("me", "help me")
async def help_me_command(ctx: tanjun.abc.SlashContext) -> None:
    ...

component = tanjun.Component().add_slash_command(help_group)
Notes
  • Unlike message command grups, slash command groups cannot be callable functions themselves.
  • Under the standard implementation, is_global is used to determine whether the command should be bulk set by tanjun.Client.set_global_commands or when set_global_commands is True
Parameters
  • name (str): The name of the command group.

    This must match the regex ^[\w-]{1,32}$ in Unicode mode and be lowercase.

  • description (str): The description of the command group.
Other Parameters
  • default_permission (bool): Whether this command can be accessed without set permissions.

    Defaults to True, meaning that users can access the command by default.

  • default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.

    If this is left as None then the default set on the parent command(s), component or client will be in effect.

  • is_global (bool): Whether this command is a global command. Defaults to True.
Returns
  • SlashCommandGroup: The command group.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the command name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the command name has uppercase characters.
    • If the description is over 100 characters long.
View Source
class MessageCommand(PartialCommand[abc.MessageContext], abc.MessageCommand[abc.CommandCallbackSigT]):
    """Standard implementation of a message command."""

    __slots__ = ("_callback", "_names", "_parent", "_parser", "_wrapped_command")

    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        /,
        *names: str,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        """Initialise a message command.

        Parameters
        ----------
        callback : collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]]
            Callback to execute when the command is invoked.

            This should be an asynchronous callback which takes one positional
            argument of type `tanjun.abc.MessageContext`, returns `None` and may use
            dependency injection to access other services.
        name : str
            The command name.

        Other Parameters
        ----------------
        *names : str
            Variable positional arguments of other names for the command.
        """
        super().__init__()
        if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)):
            callback = typing.cast(abc.CommandCallbackSigT, callback.callback)

        self._callback = injecting.CallbackDescriptor[None](callback)
        self._names = list(dict.fromkeys((name, *names)))
        self._parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None
        self._parser: typing.Optional[abc.MessageParser] = None
        self._wrapped_command = _wrapped_command

    def __repr__(self) -> str:
        return f"Command <{self._names}>"

    if typing.TYPE_CHECKING:
        __call__: abc.CommandCallbackSigT

    else:

        async def __call__(self, *args, **kwargs) -> None:
            await self._callback.callback(*args, **kwargs)

    @property
    def callback(self) -> abc.CommandCallbackSigT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        return typing.cast(abc.CommandCallbackSigT, self._callback.callback)

    @property
    # <<inherited docstring from tanjun.abc.MessageCommand>>.
    def names(self) -> collections.Collection[str]:
        return self._names.copy()

    @property
    def needs_injector(self) -> bool:
        return self._callback.needs_injector

    @property
    def parent(self) -> typing.Optional[abc.MessageCommandGroup[typing.Any]]:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        return self._parent

    @property
    def parser(self) -> typing.Optional[abc.MessageParser]:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        return self._parser

    def bind_client(self: _MessageCommandT, client: abc.Client, /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_client(client)
        if self._parser:
            self._parser.bind_client(client)

        return self

    def bind_component(self: _MessageCommandT, component: abc.Component, /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_component(component)
        if self._parser:
            self._parser.bind_component(component)

        return self

    def copy(
        self: _MessageCommandT,
        *,
        parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None,
        _new: bool = True,
    ) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if not _new:
            self._callback = copy.copy(self._callback)
            self._names = self._names.copy()
            self._parent = parent
            self._parser = self._parser.copy() if self._parser else None
            return super().copy(_new=_new)

        return super().copy(_new=_new)

    def set_parent(
        self: _MessageCommandT, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]], /
    ) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        self._parent = parent
        return self

    def set_parser(self: _MessageCommandT, parser: typing.Optional[abc.MessageParser], /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        self._parser = parser
        return self

    async def check_context(self, ctx: abc.MessageContext, /) -> bool:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        ctx.set_command(self)
        result = await utilities.gather_checks(ctx, self._checks)
        ctx.set_command(None)
        return result

    async def execute(
        self,
        ctx: abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        ctx = ctx.set_command(self)
        own_hooks = self._hooks or _EMPTY_HOOKS
        try:
            await own_hooks.trigger_pre_execution(ctx, hooks=hooks)

            if self._parser is not None:
                kwargs = await self._parser.parse(ctx)

            else:
                kwargs = _EMPTY_DICT

            await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)

        except errors.CommandError as exc:
            response = exc.message if len(exc.message) <= 2000 else exc.message[:1997] + "..."
            await ctx.respond(content=response)

        except errors.HaltExecution:
            raise

        except Exception as exc:
            if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0:
                raise

        else:
            # TODO: how should this be handled around CommandError?
            await own_hooks.trigger_success(ctx, hooks=hooks)

        finally:
            await own_hooks.trigger_post_execution(ctx, hooks=hooks)

    def load_into_component(self, component: abc.Component, /) -> None:
        # <<inherited docstring from tanjun.components.load_into_component>>.
        if not self._parent:
            component.add_message_command(self)

        if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader):
            self._wrapped_command.load_into_component(component)

Standard implementation of a message command.

#   MessageCommand( callback: ~CommandCallbackSigT, name: str, /, *names: str, _wrapped_command: Optional[tanjun.abc.ExecutableCommand[Any]] = None )
View Source
    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        /,
        *names: str,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        """Initialise a message command.

        Parameters
        ----------
        callback : collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]]
            Callback to execute when the command is invoked.

            This should be an asynchronous callback which takes one positional
            argument of type `tanjun.abc.MessageContext`, returns `None` and may use
            dependency injection to access other services.
        name : str
            The command name.

        Other Parameters
        ----------------
        *names : str
            Variable positional arguments of other names for the command.
        """
        super().__init__()
        if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)):
            callback = typing.cast(abc.CommandCallbackSigT, callback.callback)

        self._callback = injecting.CallbackDescriptor[None](callback)
        self._names = list(dict.fromkeys((name, *names)))
        self._parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None
        self._parser: typing.Optional[abc.MessageParser] = None
        self._wrapped_command = _wrapped_command

Initialise a message command.

Parameters
  • callback (collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]]): Callback to execute when the command is invoked.

    This should be an asynchronous callback which takes one positional argument of type tanjun.abc.MessageContext, returns None and may use dependency injection to access other services.

  • name (str): The command name.
Other Parameters
  • *names (str): Variable positional arguments of other names for the command.
#   callback: ~CommandCallbackSigT

Callback which is called during execution.

Note: For command groups, this is called when none of the inner-commands matches the message.

#   names: collections.abc.Collection[str]

Collection of this command's names.

#   needs_injector: bool
#   parent: Optional[tanjun.abc.MessageCommandGroup[Any]]

Parent group of this command if applicable.

#   parser: Optional[tanjun.abc.MessageParser]

Parser for this command.

#   def bind_client( self: ~_MessageCommandT, client: tanjun.abc.Client, / ) -> ~_MessageCommandT:
View Source
    def bind_client(self: _MessageCommandT, client: abc.Client, /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_client(client)
        if self._parser:
            self._parser.bind_client(client)

        return self
#   def bind_component( self: ~_MessageCommandT, component: tanjun.abc.Component, / ) -> ~_MessageCommandT:
View Source
    def bind_component(self: _MessageCommandT, component: abc.Component, /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_component(component)
        if self._parser:
            self._parser.bind_component(component)

        return self
#   def copy( self: ~_MessageCommandT, *, parent: Optional[tanjun.abc.MessageCommandGroup[Any]] = None, _new: bool = True ) -> ~_MessageCommandT:
View Source
    def copy(
        self: _MessageCommandT,
        *,
        parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None,
        _new: bool = True,
    ) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if not _new:
            self._callback = copy.copy(self._callback)
            self._names = self._names.copy()
            self._parent = parent
            self._parser = self._parser.copy() if self._parser else None
            return super().copy(_new=_new)

        return super().copy(_new=_new)

Create a copy of this command.

Other Parameters
  • parent (typing.Optional[MessageCommandGroup[tping.Any]]): The parent of the copy.
Returns
  • Self: The copy.
#   def set_parent( self: ~_MessageCommandT, parent: Optional[tanjun.abc.MessageCommandGroup[Any]], / ) -> ~_MessageCommandT:
View Source
    def set_parent(
        self: _MessageCommandT, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]], /
    ) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        self._parent = parent
        return self

Set the parent of this command.

Parameters
  • parent (typing.Optional[MessageCommandGroup[typing.Any]]): The parent of this command.
Returns
  • Self: The command instance to enable chained calls.
#   def set_parser( self: ~_MessageCommandT, parser: Optional[tanjun.abc.MessageParser], / ) -> ~_MessageCommandT:
View Source
    def set_parser(self: _MessageCommandT, parser: typing.Optional[abc.MessageParser], /) -> _MessageCommandT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        self._parser = parser
        return self

Set the for this message command.

Parameters
  • parser (MessageParser): The parser to set.
Returns
  • Self: The command instance to enable chained calls.
#   async def check_context(self, ctx: tanjun.abc.MessageContext, /) -> bool:
View Source
    async def check_context(self, ctx: abc.MessageContext, /) -> bool:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        ctx.set_command(self)
        result = await utilities.gather_checks(ctx, self._checks)
        ctx.set_command(None)
        return result
#   async def execute( self, ctx: tanjun.abc.MessageContext, /, *, hooks: Optional[collections.abc.MutableSet[tanjun.abc.Hooks[tanjun.abc.MessageContext]]] = None ) -> None:
View Source
    async def execute(
        self,
        ctx: abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        ctx = ctx.set_command(self)
        own_hooks = self._hooks or _EMPTY_HOOKS
        try:
            await own_hooks.trigger_pre_execution(ctx, hooks=hooks)

            if self._parser is not None:
                kwargs = await self._parser.parse(ctx)

            else:
                kwargs = _EMPTY_DICT

            await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)

        except errors.CommandError as exc:
            response = exc.message if len(exc.message) <= 2000 else exc.message[:1997] + "..."
            await ctx.respond(content=response)

        except errors.HaltExecution:
            raise

        except Exception as exc:
            if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0:
                raise

        else:
            # TODO: how should this be handled around CommandError?
            await own_hooks.trigger_success(ctx, hooks=hooks)

        finally:
            await own_hooks.trigger_post_execution(ctx, hooks=hooks)
#   def load_into_component(self, component: tanjun.abc.Component, /) -> None:
View Source
    def load_into_component(self, component: abc.Component, /) -> None:
        # <<inherited docstring from tanjun.components.load_into_component>>.
        if not self._parent:
            component.add_message_command(self)

        if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader):
            self._wrapped_command.load_into_component(component)

Load the object into the component.

Parameters
View Source
class MessageCommandGroup(MessageCommand[abc.CommandCallbackSigT], abc.MessageCommandGroup[abc.CommandCallbackSigT]):
    """Standard implementation of a message command group."""

    __slots__ = ("_commands", "_is_strict", "_names_to_commands")

    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        /,
        *names: str,
        strict: bool = False,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        """Initialise a message command group.

        Parameters
        ----------
        name : str
            The command name.

        Other Parameters
        ----------------
        *names : str
            Variable positional arguments of other names for the command.
        strict : bool
            Whether this command group should only allow commands without spaces in their names.

            This allows for a more optimised command search pattern to be used and
            enforces that command names are unique to a single command within the group.
        """
        super().__init__(callback, name, *names, _wrapped_command=_wrapped_command)
        self._commands: list[abc.MessageCommand[typing.Any]] = []
        self._is_strict = strict
        self._names_to_commands: dict[str, abc.MessageCommand[typing.Any]] = {}

    def __repr__(self) -> str:
        return f"CommandGroup <{len(self._commands)}: {self._names}>"

    @property
    def commands(self) -> collections.Collection[abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.MessageCommandGroup>>.
        return self._commands.copy()

    @property
    def is_strict(self) -> bool:
        return self._is_strict

    def copy(
        self: _MessageCommandGroupT,
        *,
        parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None,
        _new: bool = True,
    ) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if not _new:
            commands = {command: command.copy(parent=self) for command in self._commands}
            self._commands = list(commands.values())
            self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()}
            return super().copy(parent=parent, _new=_new)

        return super().copy(parent=parent, _new=_new)

    def add_command(self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /) -> _MessageCommandGroupT:
        """Add a command to this group.

        Parameters
        ----------
        command : MessageCommand
            The command to add.

        Returns
        -------
        Self
            The group instance to enable chained calls.

        Raises
        ------
        ValueError
            If one of the command's names is already registered in a strict
            command group.
        """
        if command in self._commands:
            return self

        if self._is_strict:
            if any(" " in name for name in command.names):
                raise ValueError("Sub-command names may not contain spaces in a strict message command group")

            if name_conflicts := self._names_to_commands.keys() & command.names:
                raise ValueError(
                    "Sub-command names must be unique in a strict message command group. "
                    "The following conflicts were found " + ", ".join(name_conflicts)
                )

            self._names_to_commands.update((name, command) for name in command.names)

        command.set_parent(self)
        self._commands.append(command)
        return self

    def remove_command(
        self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /
    ) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.MessageCommandGroup>>.
        self._commands.remove(command)
        if self._is_strict:
            for name in command.names:
                if self._names_to_commands.get(name) == command:
                    del self._names_to_commands[name]

        command.set_parent(None)
        return self

    def with_command(self, command: AnyMessageCommandT, /) -> AnyMessageCommandT:
        self.add_command(command)
        return command

    def bind_client(self: _MessageCommandGroupT, client: abc.Client, /) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_client(client)
        for command in self._commands:
            command.bind_client(client)

        return self

    def bind_component(self: _MessageCommandGroupT, component: abc.Component, /) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_component(component)
        for command in self._commands:
            command.bind_component(component)

        return self

    def find_command(self, content: str, /) -> collections.Iterable[tuple[str, abc.MessageCommand[typing.Any]]]:
        if self._is_strict:
            name = content.split(" ")[0]
            if command := self._names_to_commands.get(name):
                yield name, command
            return

        for command in self._commands:
            if (name_ := utilities.match_prefix_names(content, command.names)) is not None:
                yield name_, command

    async def execute(
        self,
        ctx: abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if ctx.message.content is None:
            raise ValueError("Cannot execute a command with a content-less message")

        if self._hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._hooks)

        for name, command in self.find_command(ctx.content):
            if await command.check_context(ctx):
                content = ctx.content[len(name) :]
                lstripped_content = content.lstrip()
                space_len = len(content) - len(lstripped_content)
                ctx.set_triggering_name(ctx.triggering_name + (" " * space_len) + name)
                ctx.set_content(lstripped_content)
                await command.execute(ctx, hooks=hooks)
                return

        await super().execute(ctx, hooks=hooks)

Standard implementation of a message command group.

#   MessageCommandGroup( callback: ~CommandCallbackSigT, name: str, /, *names: str, strict: bool = False, _wrapped_command: Optional[tanjun.abc.ExecutableCommand[Any]] = None )
View Source
    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        /,
        *names: str,
        strict: bool = False,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        """Initialise a message command group.

        Parameters
        ----------
        name : str
            The command name.

        Other Parameters
        ----------------
        *names : str
            Variable positional arguments of other names for the command.
        strict : bool
            Whether this command group should only allow commands without spaces in their names.

            This allows for a more optimised command search pattern to be used and
            enforces that command names are unique to a single command within the group.
        """
        super().__init__(callback, name, *names, _wrapped_command=_wrapped_command)
        self._commands: list[abc.MessageCommand[typing.Any]] = []
        self._is_strict = strict
        self._names_to_commands: dict[str, abc.MessageCommand[typing.Any]] = {}

Initialise a message command group.

Parameters
  • name (str): The command name.
Other Parameters
  • *names (str): Variable positional arguments of other names for the command.
  • strict (bool): Whether this command group should only allow commands without spaces in their names.

    This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group.

#   commands: collections.abc.Collection[tanjun.abc.MessageCommand[typing.Any]]

Collection of the commands in this group.

Note: This may include command groups.

#   is_strict: bool
#   def copy( self: ~_MessageCommandGroupT, *, parent: Optional[tanjun.abc.MessageCommandGroup[Any]] = None, _new: bool = True ) -> ~_MessageCommandGroupT:
View Source
    def copy(
        self: _MessageCommandGroupT,
        *,
        parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None,
        _new: bool = True,
    ) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if not _new:
            commands = {command: command.copy(parent=self) for command in self._commands}
            self._commands = list(commands.values())
            self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()}
            return super().copy(parent=parent, _new=_new)

        return super().copy(parent=parent, _new=_new)

Create a copy of this command.

Other Parameters
  • parent (typing.Optional[MessageCommandGroup[tping.Any]]): The parent of the copy.
Returns
  • Self: The copy.
#   def add_command( self: ~_MessageCommandGroupT, command: tanjun.abc.MessageCommand[typing.Any], / ) -> ~_MessageCommandGroupT:
View Source
    def add_command(self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /) -> _MessageCommandGroupT:
        """Add a command to this group.

        Parameters
        ----------
        command : MessageCommand
            The command to add.

        Returns
        -------
        Self
            The group instance to enable chained calls.

        Raises
        ------
        ValueError
            If one of the command's names is already registered in a strict
            command group.
        """
        if command in self._commands:
            return self

        if self._is_strict:
            if any(" " in name for name in command.names):
                raise ValueError("Sub-command names may not contain spaces in a strict message command group")

            if name_conflicts := self._names_to_commands.keys() & command.names:
                raise ValueError(
                    "Sub-command names must be unique in a strict message command group. "
                    "The following conflicts were found " + ", ".join(name_conflicts)
                )

            self._names_to_commands.update((name, command) for name in command.names)

        command.set_parent(self)
        self._commands.append(command)
        return self

Add a command to this group.

Parameters
  • command (MessageCommand): The command to add.
Returns
  • Self: The group instance to enable chained calls.
Raises
  • ValueError: If one of the command's names is already registered in a strict command group.
#   def remove_command( self: ~_MessageCommandGroupT, command: tanjun.abc.MessageCommand[typing.Any], / ) -> ~_MessageCommandGroupT:
View Source
    def remove_command(
        self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /
    ) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.MessageCommandGroup>>.
        self._commands.remove(command)
        if self._is_strict:
            for name in command.names:
                if self._names_to_commands.get(name) == command:
                    del self._names_to_commands[name]

        command.set_parent(None)
        return self

Remove a command from this group.

Parameters
  • command (MessageCommand): The command to remove.
Raises
  • ValueError: If the provided command isn't found.
Returns
  • Self: The group instance to enable chained calls.
#   def with_command(self, command: ~AnyMessageCommandT, /) -> ~AnyMessageCommandT:
View Source
    def with_command(self, command: AnyMessageCommandT, /) -> AnyMessageCommandT:
        self.add_command(command)
        return command

Add a command to this group through a decorator call.

Parameters
  • command (MessageCommand): The command to add.
Returns
  • MessageCommand: The added command.
#   def bind_client( self: ~_MessageCommandGroupT, client: tanjun.abc.Client, / ) -> ~_MessageCommandGroupT:
View Source
    def bind_client(self: _MessageCommandGroupT, client: abc.Client, /) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_client(client)
        for command in self._commands:
            command.bind_client(client)

        return self
#   def bind_component( self: ~_MessageCommandGroupT, component: tanjun.abc.Component, / ) -> ~_MessageCommandGroupT:
View Source
    def bind_component(self: _MessageCommandGroupT, component: abc.Component, /) -> _MessageCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        super().bind_component(component)
        for command in self._commands:
            command.bind_component(component)

        return self
#   def find_command( self, content: str, / ) -> collections.abc.Iterable[tuple[str, tanjun.abc.MessageCommand[typing.Any]]]:
View Source
    def find_command(self, content: str, /) -> collections.Iterable[tuple[str, abc.MessageCommand[typing.Any]]]:
        if self._is_strict:
            name = content.split(" ")[0]
            if command := self._names_to_commands.get(name):
                yield name, command
            return

        for command in self._commands:
            if (name_ := utilities.match_prefix_names(content, command.names)) is not None:
                yield name_, command
#   async def execute( self, ctx: tanjun.abc.MessageContext, /, *, hooks: Optional[collections.abc.MutableSet[tanjun.abc.Hooks[tanjun.abc.MessageContext]]] = None ) -> None:
View Source
    async def execute(
        self,
        ctx: abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.MessageCommand>>.
        if ctx.message.content is None:
            raise ValueError("Cannot execute a command with a content-less message")

        if self._hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._hooks)

        for name, command in self.find_command(ctx.content):
            if await command.check_context(ctx):
                content = ctx.content[len(name) :]
                lstripped_content = content.lstrip()
                space_len = len(content) - len(lstripped_content)
                ctx.set_triggering_name(ctx.triggering_name + (" " * space_len) + name)
                ctx.set_content(lstripped_content)
                await command.execute(ctx, hooks=hooks)
                return

        await super().execute(ctx, hooks=hooks)
View Source
class SlashCommand(BaseSlashCommand, abc.SlashCommand[abc.CommandCallbackSigT]):
    """Standard implementation of a slash command."""

    __slots__ = ("_always_defer", "_builder", "_callback", "_client", "_tracked_options", "_wrapped_command")

    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        description: str,
        /,
        *,
        always_defer: bool = False,
        default_permission: bool = True,
        default_to_ephemeral: typing.Optional[bool] = None,
        is_global: bool = True,
        sort_options: bool = True,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        r"""Initialise a slash command.

        .. note::
            Under the standard implementation, `is_global` is used to determine whether
            the command should be bulk set by `tanjun.Client.set_global_commands`
            or when `set_global_commands` is True

        .. warning::
            `default_permission` and `is_global` are ignored for commands within
            slash command groups.

        Parameters
        ----------
        callback : collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]]
            Callback to execute when the command is invoked.

            This should be an asynchronous callback which takes one positional
            argument of type `tanjun.abc.SlashContext`, returns `None` and may use
            dependency injection to access other services.
        name : str
            The command's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The command's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        always_defer : bool
            Whether the contexts this command is executed with should always be deferred
            before being passed to the command's callback.

            Defaults to `False`.

            .. note::
                The ephemeral state of the first response is decided by whether the
                deferral is ephemeral.
        default_permission : bool
            Whether this command can be accessed without set permissions.

            Defaults to `True`, meaning that users can access the command by default.
        default_to_ephemeral : typing.Optional[bool]
            Whether this command's responses should default to ephemeral unless flags
            are set to override this.

            If this is left as `None` then the default set on the parent command(s),
            component or client will be in effect.
        is_global : bool
            Whether this command is a global command. Defaults to `True`.
        sort_options : bool
            Whether this command should sort its set options based on whether
            they're required.

            If this is `True` then the options are re-sorted to meet the requirement
            from Discord that required command options be listed before optional
            ones.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the command name has uppercase characters.
            * If the description is over 100 characters long.
        """
        super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global)
        if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)):
            callback = typing.cast(abc.CommandCallbackSigT, callback.callback)

        self._always_defer = always_defer
        self._builder = _CommandBuilder(name, description, sort_options).set_default_permission(default_permission)
        self._callback = injecting.CallbackDescriptor[None](callback)
        self._client: typing.Optional[abc.Client] = None
        self._tracked_options: dict[str, _TrackedOption] = {}
        self._wrapped_command = _wrapped_command

    if typing.TYPE_CHECKING:
        __call__: abc.CommandCallbackSigT

    else:

        async def __call__(self, *args, **kwargs) -> None:
            await self._callback.callback(*args, **kwargs)

    @property
    def callback(self) -> abc.CommandCallbackSigT:
        # <<inherited docstring from tanjun.abc.SlashCommand>>.
        return typing.cast(abc.CommandCallbackSigT, self._callback.callback)

    @property
    def needs_injector(self) -> bool:
        return (
            self._callback.needs_injector
            or any(option.needs_injector for option in self._tracked_options.values())
            or super().needs_injector
        )

    def bind_client(self: _SlashCommandT, client: abc.Client, /) -> _SlashCommandT:
        self._client = client
        super().bind_client(client)
        for option in self._tracked_options.values():
            option.check_client(client)

        return self

    def build(self) -> special_endpoints_api.CommandBuilder:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._builder.sort().copy()

    def load_into_component(self, component: abc.Component, /) -> None:
        super().load_into_component(component)
        if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader):
            self._wrapped_command.load_into_component(component)

    def _add_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        type_: typing.Union[hikari.OptionType, int] = hikari.OptionType.STRING,
        /,
        *,
        always_float: bool = False,
        channel_types: typing.Optional[collections.Sequence[int]] = None,
        choices: typing.Union[
            collections.Mapping[str, typing.Union[str, int, float]], collections.Sequence[typing.Any], None
        ] = None,
        converters: typing.Union[collections.Iterable[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Union[int, float, None] = None,
        max_value: typing.Union[int, float, None] = None,
        only_member: bool = False,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        _validate_name(name)
        if len(description) > 100:
            raise ValueError("The option description cannot be over 100 characters in length")

        if len(self._builder.options) == 25:
            raise ValueError("Slash commands cannot have more than 25 options")

        if min_value and max_value and min_value > max_value:
            raise ValueError("The min value cannot be greater than the max value")

        type_ = hikari.OptionType(type_)
        if isinstance(converters, collections.Iterable):
            converters_ = list(map(_convert_to_injectable, converters))

        else:
            converters_ = [_convert_to_injectable(converters)]

        if self._client:
            for converter in converters_:
                if isinstance(converter.callback, conversion.BaseConverter):
                    converter.callback.check_client(self._client, f"{self._name}'s slash option '{name}'")

        if choices is None:
            actual_choices: typing.Optional[list[hikari.CommandChoice]] = None

        elif isinstance(choices, collections.Mapping):
            actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices.items()]

        else:
            warnings.warn(
                "Passing a sequence of tuples to `choices` is deprecated since 2.1.2a1, "
                "please pass a mapping instead.",
                category=DeprecationWarning,
                stacklevel=2 + _stack_level,
            )
            actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices]

        if actual_choices and len(actual_choices) > 25:
            raise ValueError("Slash command options cannot have more than 25 choices")

        required = default is _UNDEFINED_DEFAULT
        self._builder.add_option(
            hikari.CommandOption(
                type=type_,
                name=name,
                description=description,
                is_required=required,
                choices=actual_choices,
                channel_types=channel_types,
                min_value=min_value,
                max_value=max_value,
            )
        )
        if pass_as_kwarg:
            self._tracked_options[name] = _TrackedOption(
                name=name,
                option_type=type_,
                always_float=always_float,
                converters=converters_,
                default=default,
                only_member=only_member,
            )
        return self

    def add_str_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None,
        converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add a string option to the slash command.

        .. note::
            As a shorthand, `choices` also supports passing a list of strings
            rather than a dict of names to values (each string will used as
            both the choice's name and value with the names being capitalised).

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        choices : typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None]
            The option's choices.

            This either a mapping of [option_name, option_value] where both option_name
            and option_value should be strings of up to 100 characters or a sequence
            of strings where the string will be used for both the choice's name and
            value.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the `coverters` field will be ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
        """
        if choices is None:
            actual_choices = None

        elif isinstance(choices, collections.Mapping):
            actual_choices = choices

        else:
            actual_choices = {}
            warned = False
            for choice in choices:
                if isinstance(choice, tuple):  # type: ignore[unreachable]  # the point of this is for deprecation
                    if not warned:  # type: ignore[unreachable]  # mypy sees `warned = True` and messes up.
                        warnings.warn(
                            "Passing a sequence of tuples for 'choices' is deprecated since 2.1.2a1, "
                            "please pass a mapping instead.",
                            category=DeprecationWarning,
                            stacklevel=2 + _stack_level,
                        )
                        warned = True

                    actual_choices[choice[0]] = choice[1]

                else:
                    actual_choices[choice.capitalize()] = choice

        return self._add_option(
            name,
            description,
            hikari.OptionType.STRING,
            choices=actual_choices,
            converters=converters,
            default=default,
            pass_as_kwarg=pass_as_kwarg,
        )

    def add_int_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        choices: typing.Optional[collections.Mapping[str, int]] = None,
        converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Optional[int] = None,
        max_value: typing.Optional[int] = None,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add an integer option to the slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        choices : typing.Optional[collections.abc.Mapping[str, int]]
            The option's choices.

            This is a mapping of [option_name, option_value] where option_name
            should be a string of up to 100 characters and option_value should
            be an integer.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        min_value : typing.Optional[int]
            The option's (inclusive) minimum value.

            Defaults to no minimum value.
        max_value : typing.Optional[int]
            The option's (inclusive) maximum value.

            Defaults to no minimum value.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the `coverters` field will be ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
            * If `min_value` is greater than `max_value`.
        """
        return self._add_option(
            name,
            description,
            hikari.OptionType.INTEGER,
            choices=choices,
            converters=converters,
            default=default,
            min_value=min_value,
            max_value=max_value,
            pass_as_kwarg=pass_as_kwarg,
            _stack_level=_stack_level + 1,
        )

    def add_float_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        always_float: bool = True,
        choices: typing.Optional[collections.Mapping[str, float]] = None,
        converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Optional[float] = None,
        max_value: typing.Optional[float] = None,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add a float option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        always_float : bool
            If this is set to `True` then the value will always be converted to a
            float (this will happen before it's passed to converters).

            This masks behaviour from Discord where we will either be provided a `float`
            or `int` dependent on what the user provided and defaults to `True`.
        choices : typing.Optional[collections.abc.Mapping[str, float]]
            The option's choices.

            This is a mapping of [option_name, option_value] where option_name
            should be a string of up to 100 characters and option_value should
            be a float.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        min_value : typing.Optional[float]
            The option's (inclusive) minimum value.

            Defaults to no minimum value.
        max_value : typing.Optional[float]
            The option's (inclusive) maximum value.

            Defaults to no minimum value.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the fields `coverters`, and `always_float` will be
            ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
            * If `min_value` is greater than `max_value`.
        """
        return self._add_option(
            name,
            description,
            hikari.OptionType.FLOAT,
            choices=choices,
            converters=converters,
            default=default,
            min_value=float(min_value) if min_value is not None else None,
            max_value=float(max_value) if max_value is not None else None,
            pass_as_kwarg=pass_as_kwarg,
            always_float=always_float,
            _stack_level=_stack_level + 1,
        )

    def add_bool_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a boolean option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(
            name, description, hikari.OptionType.BOOLEAN, default=default, pass_as_kwarg=pass_as_kwarg
        )

    def add_user_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a user option to a slash command.

        .. note::
            This may result in `hikari.InteractionMember` or
            `hikari.users.User` if the user isn't in the current guild or if this
            command was executed in a DM channel.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.USER, default=default, pass_as_kwarg=pass_as_kwarg)

    def add_member_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
    ) -> _SlashCommandT:
        r"""Add a member option to a slash command.

        .. note::
            This will always result in `hikari.InteractionMember`.

        .. warning::
            Unlike the other options, this is an artificial option which adds
            a restraint to the USER option type and therefore cannot have
            `pass_as_kwarg` set to `False` as this artificial constaint isn't
            present when its not being passed as a keyword argument.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.USER, default=default, only_member=True)

    def add_channel_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        types: typing.Optional[collections.Collection[type[hikari.PartialChannel]]] = None,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a channel option to a slash command.

        .. note::
            This will always result in `hikari.InteractionChannel`.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Parameters
        ----------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        types : typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]]
            A collection of the channel classes this option should accept.

            If left as `None` or empty then the option will allow all channel types.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
            * If an invalid type is passed in `types`.
        """
        import itertools

        if types:
            try:
                channel_types = list(set(itertools.chain.from_iterable(map(_channel_types.__getitem__, types))))

            except KeyError as exc:
                raise ValueError(f"Unknown channel type {exc.args[0]}") from exc

        else:
            channel_types = None

        return self._add_option(
            name,
            description,
            hikari.OptionType.CHANNEL,
            channel_types=channel_types,
            default=default,
            pass_as_kwarg=pass_as_kwarg,
        )

    def add_role_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a role option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.ROLE, default=default, pass_as_kwarg=pass_as_kwarg)

    def add_mentionable_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a mentionable option to a slash command.

        .. note::
            This may target roles, guild members or users and results in
            `Union[hikari.User, hikari.InteractionMember, hikari.Role]`.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(
            name, description, hikari.OptionType.MENTIONABLE, default=default, pass_as_kwarg=pass_as_kwarg
        )

    async def _process_args(self, ctx: abc.SlashContext, /) -> collections.Mapping[str, typing.Any]:
        keyword_args: dict[str, typing.Union[int, float, str, hikari.User, hikari.Role, hikari.InteractionChannel]] = {}
        for tracked_option in self._tracked_options.values():
            if not (option := ctx.options.get(tracked_option.name)):
                if tracked_option.default is _UNDEFINED_DEFAULT:
                    raise RuntimeError(  # TODO: ConversionError?
                        f"Required option {tracked_option.name} is missing data, are you sure your commands"
                        " are up to date?"
                    )

                else:
                    keyword_args[tracked_option.name] = tracked_option.default

            elif option.type is hikari.OptionType.USER:
                member: typing.Optional[hikari.InteractionMember] = None
                if tracked_option.is_only_member and not (member := option.resolve_to_member(default=None)):
                    raise errors.ConversionError(
                        f"Couldn't find member for provided user: {option.value}", tracked_option.name
                    )

                keyword_args[option.name] = member or option.resolve_to_user()

            elif option.type is hikari.OptionType.CHANNEL:
                keyword_args[option.name] = option.resolve_to_channel()

            elif option.type is hikari.OptionType.ROLE:
                keyword_args[option.name] = option.resolve_to_role()

            elif option.type is hikari.OptionType.MENTIONABLE:
                keyword_args[option.name] = option.resolve_to_mentionable()

            else:
                value = option.value
                # To be type safe we obfuscate the fact that discord's double type will provide an int or float
                # depending on the value Discord inputs by always casting to float.
                if tracked_option.type is hikari.OptionType.FLOAT and tracked_option.is_always_float:
                    value = float(value)

                if tracked_option.converters:
                    value = await tracked_option.convert(ctx, option.value)

                keyword_args[option.name] = value

        return keyword_args

    async def execute(
        self,
        ctx: abc.SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        if self._always_defer and not ctx.has_been_deferred and not ctx.has_responded:
            await ctx.defer()

        ctx = ctx.set_command(self)
        own_hooks = self._hooks or _EMPTY_HOOKS
        try:
            await own_hooks.trigger_pre_execution(ctx, hooks=hooks)

            if self._tracked_options:
                kwargs = await self._process_args(ctx)

            else:
                kwargs = _EMPTY_DICT

            await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)

        except errors.CommandError as exc:
            await ctx.respond(exc.message)

        except errors.HaltExecution:
            # Unlike a message command, this won't necessarily reach the client level try except
            # block so we have to handle this here.
            await ctx.mark_not_found()

        except Exception as exc:
            if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0:
                raise

        else:
            await own_hooks.trigger_success(ctx, hooks=hooks)

        finally:
            await own_hooks.trigger_post_execution(ctx, hooks=hooks)

    def copy(
        self: _SlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _SlashCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._callback = copy.copy(self._callback)
            return super().copy(_new=_new, parent=parent)

        return super().copy(_new=_new, parent=parent)

Standard implementation of a slash command.

#   SlashCommand( callback: ~CommandCallbackSigT, name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: Optional[bool] = None, is_global: bool = True, sort_options: bool = True, _wrapped_command: Optional[tanjun.abc.ExecutableCommand[Any]] = None )
View Source
    def __init__(
        self,
        callback: abc.CommandCallbackSigT,
        name: str,
        description: str,
        /,
        *,
        always_defer: bool = False,
        default_permission: bool = True,
        default_to_ephemeral: typing.Optional[bool] = None,
        is_global: bool = True,
        sort_options: bool = True,
        _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None,
    ) -> None:
        r"""Initialise a slash command.

        .. note::
            Under the standard implementation, `is_global` is used to determine whether
            the command should be bulk set by `tanjun.Client.set_global_commands`
            or when `set_global_commands` is True

        .. warning::
            `default_permission` and `is_global` are ignored for commands within
            slash command groups.

        Parameters
        ----------
        callback : collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]]
            Callback to execute when the command is invoked.

            This should be an asynchronous callback which takes one positional
            argument of type `tanjun.abc.SlashContext`, returns `None` and may use
            dependency injection to access other services.
        name : str
            The command's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The command's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        always_defer : bool
            Whether the contexts this command is executed with should always be deferred
            before being passed to the command's callback.

            Defaults to `False`.

            .. note::
                The ephemeral state of the first response is decided by whether the
                deferral is ephemeral.
        default_permission : bool
            Whether this command can be accessed without set permissions.

            Defaults to `True`, meaning that users can access the command by default.
        default_to_ephemeral : typing.Optional[bool]
            Whether this command's responses should default to ephemeral unless flags
            are set to override this.

            If this is left as `None` then the default set on the parent command(s),
            component or client will be in effect.
        is_global : bool
            Whether this command is a global command. Defaults to `True`.
        sort_options : bool
            Whether this command should sort its set options based on whether
            they're required.

            If this is `True` then the options are re-sorted to meet the requirement
            from Discord that required command options be listed before optional
            ones.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the command name has uppercase characters.
            * If the description is over 100 characters long.
        """
        super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global)
        if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)):
            callback = typing.cast(abc.CommandCallbackSigT, callback.callback)

        self._always_defer = always_defer
        self._builder = _CommandBuilder(name, description, sort_options).set_default_permission(default_permission)
        self._callback = injecting.CallbackDescriptor[None](callback)
        self._client: typing.Optional[abc.Client] = None
        self._tracked_options: dict[str, _TrackedOption] = {}
        self._wrapped_command = _wrapped_command

Initialise a slash command.

Note: Under the standard implementation, is_global is used to determine whether the command should be bulk set by tanjun.Client.set_global_commands or when set_global_commands is True

Warning: default_permission and is_global are ignored for commands within slash command groups.

Parameters
  • callback (collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]]): Callback to execute when the command is invoked.

    This should be an asynchronous callback which takes one positional argument of type tanjun.abc.SlashContext, returns None and may use dependency injection to access other services.

  • name (str): The command's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The command's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • always_defer (bool): Whether the contexts this command is executed with should always be deferred before being passed to the command's callback.

    Defaults to False.

    Note: The ephemeral state of the first response is decided by whether the deferral is ephemeral.

  • default_permission (bool): Whether this command can be accessed without set permissions.

    Defaults to True, meaning that users can access the command by default.

  • default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.

    If this is left as None then the default set on the parent command(s), component or client will be in effect.

  • is_global (bool): Whether this command is a global command. Defaults to True.
  • sort_options (bool): Whether this command should sort its set options based on whether they're required.

    If this is True then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones.

Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the command name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the command name has uppercase characters.
    • If the description is over 100 characters long.
#   callback: ~CommandCallbackSigT

Callback which is called during execution.

#   needs_injector: bool
#   def bind_client( self: ~_SlashCommandT, client: tanjun.abc.Client, / ) -> ~_SlashCommandT:
View Source
    def bind_client(self: _SlashCommandT, client: abc.Client, /) -> _SlashCommandT:
        self._client = client
        super().bind_client(client)
        for option in self._tracked_options.values():
            option.check_client(client)

        return self
#   def build(self) -> hikari.api.special_endpoints.CommandBuilder:
View Source
    def build(self) -> special_endpoints_api.CommandBuilder:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        return self._builder.sort().copy()

Get a builder object for this command.

Returns
  • hikari.api.CommandBuilder: A builder object for this command. Use to declare this command on globally or for a specific guild.
#   def load_into_component(self, component: tanjun.abc.Component, /) -> None:
View Source
    def load_into_component(self, component: abc.Component, /) -> None:
        super().load_into_component(component)
        if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader):
            self._wrapped_command.load_into_component(component)

Load the object into the component.

Parameters
#   def add_str_option( self: ~_SlashCommandT, name: str, description: str, /, *, choices: Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], NoneType] = None, converters: Union[collections.abc.Sequence[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any = <object object>, pass_as_kwarg: bool = True, _stack_level: int = 0 ) -> ~_SlashCommandT:
View Source
    def add_str_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None,
        converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add a string option to the slash command.

        .. note::
            As a shorthand, `choices` also supports passing a list of strings
            rather than a dict of names to values (each string will used as
            both the choice's name and value with the names being capitalised).

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        choices : typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None]
            The option's choices.

            This either a mapping of [option_name, option_value] where both option_name
            and option_value should be strings of up to 100 characters or a sequence
            of strings where the string will be used for both the choice's name and
            value.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the `coverters` field will be ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
        """
        if choices is None:
            actual_choices = None

        elif isinstance(choices, collections.Mapping):
            actual_choices = choices

        else:
            actual_choices = {}
            warned = False
            for choice in choices:
                if isinstance(choice, tuple):  # type: ignore[unreachable]  # the point of this is for deprecation
                    if not warned:  # type: ignore[unreachable]  # mypy sees `warned = True` and messes up.
                        warnings.warn(
                            "Passing a sequence of tuples for 'choices' is deprecated since 2.1.2a1, "
                            "please pass a mapping instead.",
                            category=DeprecationWarning,
                            stacklevel=2 + _stack_level,
                        )
                        warned = True

                    actual_choices[choice[0]] = choice[1]

                else:
                    actual_choices[choice.capitalize()] = choice

        return self._add_option(
            name,
            description,
            hikari.OptionType.STRING,
            choices=actual_choices,
            converters=converters,
            default=default,
            pass_as_kwarg=pass_as_kwarg,
        )

Add a string option to the slash command.

Note: As a shorthand, choices also supports passing a list of strings rather than a dict of names to values (each string will used as both the choice's name and value with the names being capitalised).

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • choices (typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None]): The option's choices.

    This either a mapping of [option_name, option_value] where both option_name and option_value should be strings of up to 100 characters or a sequence of strings where the string will be used for both the choice's name and value.

  • converters (typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig]): The option's converters.

    This may be either one or multiple ConverterSig callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used and the coverters field will be ignored.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the option has more than 25 choices.
    • If the command already has 25 options.
#   def add_int_option( self: ~_SlashCommandT, name: str, description: str, /, *, choices: Optional[collections.abc.Mapping[str, int]] = None, converters: Union[collections.abc.Collection[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any = <object object>, min_value: Optional[int] = None, max_value: Optional[int] = None, pass_as_kwarg: bool = True, _stack_level: int = 0 ) -> ~_SlashCommandT:
View Source
    def add_int_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        choices: typing.Optional[collections.Mapping[str, int]] = None,
        converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Optional[int] = None,
        max_value: typing.Optional[int] = None,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add an integer option to the slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        choices : typing.Optional[collections.abc.Mapping[str, int]]
            The option's choices.

            This is a mapping of [option_name, option_value] where option_name
            should be a string of up to 100 characters and option_value should
            be an integer.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        min_value : typing.Optional[int]
            The option's (inclusive) minimum value.

            Defaults to no minimum value.
        max_value : typing.Optional[int]
            The option's (inclusive) maximum value.

            Defaults to no minimum value.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the `coverters` field will be ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
            * If `min_value` is greater than `max_value`.
        """
        return self._add_option(
            name,
            description,
            hikari.OptionType.INTEGER,
            choices=choices,
            converters=converters,
            default=default,
            min_value=min_value,
            max_value=max_value,
            pass_as_kwarg=pass_as_kwarg,
            _stack_level=_stack_level + 1,
        )

Add an integer option to the slash command.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • choices (typing.Optional[collections.abc.Mapping[str, int]]): The option's choices.

    This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be an integer.

  • converters (typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]): The option's converters.

    This may be either one or multiple ConverterSig callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • min_value (typing.Optional[int]): The option's (inclusive) minimum value.

    Defaults to no minimum value.

  • max_value (typing.Optional[int]): The option's (inclusive) maximum value.

    Defaults to no minimum value.

  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used and the coverters field will be ignored.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the option has more than 25 choices.
    • If the command already has 25 options.
    • If min_value is greater than max_value.
#   def add_float_option( self: ~_SlashCommandT, name: str, description: str, /, *, always_float: bool = True, choices: Optional[collections.abc.Mapping[str, float]] = None, converters: Union[collections.abc.Collection[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any = <object object>, min_value: Optional[float] = None, max_value: Optional[float] = None, pass_as_kwarg: bool = True, _stack_level: int = 0 ) -> ~_SlashCommandT:
View Source
    def add_float_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        always_float: bool = True,
        choices: typing.Optional[collections.Mapping[str, float]] = None,
        converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
        default: typing.Any = _UNDEFINED_DEFAULT,
        min_value: typing.Optional[float] = None,
        max_value: typing.Optional[float] = None,
        pass_as_kwarg: bool = True,
        _stack_level: int = 0,
    ) -> _SlashCommandT:
        r"""Add a float option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        always_float : bool
            If this is set to `True` then the value will always be converted to a
            float (this will happen before it's passed to converters).

            This masks behaviour from Discord where we will either be provided a `float`
            or `int` dependent on what the user provided and defaults to `True`.
        choices : typing.Optional[collections.abc.Mapping[str, float]]
            The option's choices.

            This is a mapping of [option_name, option_value] where option_name
            should be a string of up to 100 characters and option_value should
            be a float.
        converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]
            The option's converters.

            This may be either one or multiple `ConverterSig` callbacks used to
            convert the option's value to the final form.
            If no converters are provided then the raw value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        min_value : typing.Optional[float]
            The option's (inclusive) minimum value.

            Defaults to no minimum value.
        max_value : typing.Optional[float]
            The option's (inclusive) maximum value.

            Defaults to no minimum value.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used and the fields `coverters`, and `always_float` will be
            ignored.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
            * If `min_value` is greater than `max_value`.
        """
        return self._add_option(
            name,
            description,
            hikari.OptionType.FLOAT,
            choices=choices,
            converters=converters,
            default=default,
            min_value=float(min_value) if min_value is not None else None,
            max_value=float(max_value) if max_value is not None else None,
            pass_as_kwarg=pass_as_kwarg,
            always_float=always_float,
            _stack_level=_stack_level + 1,
        )

Add a float option to a slash command.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • always_float (bool): If this is set to True then the value will always be converted to a float (this will happen before it's passed to converters).

    This masks behaviour from Discord where we will either be provided a float or int dependent on what the user provided and defaults to True.

  • choices (typing.Optional[collections.abc.Mapping[str, float]]): The option's choices.

    This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be a float.

  • converters (typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]): The option's converters.

    This may be either one or multiple ConverterSig callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • min_value (typing.Optional[float]): The option's (inclusive) minimum value.

    Defaults to no minimum value.

  • max_value (typing.Optional[float]): The option's (inclusive) maximum value.

    Defaults to no minimum value.

  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used and the fields coverters, and always_float will be ignored.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the option has more than 25 choices.
    • If the command already has 25 options.
    • If min_value is greater than max_value.
#   def add_bool_option( self: ~_SlashCommandT, name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> ~_SlashCommandT:
View Source
    def add_bool_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a boolean option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(
            name, description, hikari.OptionType.BOOLEAN, default=default, pass_as_kwarg=pass_as_kwarg
        )

Add a boolean option to a slash command.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the command already has 25 options.
#   def add_user_option( self: ~_SlashCommandT, name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> ~_SlashCommandT:
View Source
    def add_user_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a user option to a slash command.

        .. note::
            This may result in `hikari.InteractionMember` or
            `hikari.users.User` if the user isn't in the current guild or if this
            command was executed in a DM channel.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the option has more than 25 choices.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.USER, default=default, pass_as_kwarg=pass_as_kwarg)

Add a user option to a slash command.

Note: This may result in hikari.InteractionMember or hikari.users.User if the user isn't in the current guild or if this command was executed in a DM channel.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the option has more than 25 choices.
    • If the command already has 25 options.
#   def add_member_option( self: ~_SlashCommandT, name: str, description: str, /, *, default: Any = <object object> ) -> ~_SlashCommandT:
View Source
    def add_member_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
    ) -> _SlashCommandT:
        r"""Add a member option to a slash command.

        .. note::
            This will always result in `hikari.InteractionMember`.

        .. warning::
            Unlike the other options, this is an artificial option which adds
            a restraint to the USER option type and therefore cannot have
            `pass_as_kwarg` set to `False` as this artificial constaint isn't
            present when its not being passed as a keyword argument.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.USER, default=default, only_member=True)

Add a member option to a slash command.

Note: This will always result in hikari.InteractionMember.

Warning: Unlike the other options, this is an artificial option which adds a restraint to the USER option type and therefore cannot have pass_as_kwarg set to False as this artificial constaint isn't present when its not being passed as a keyword argument.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the command already has 25 options.
#   def add_channel_option( self: ~_SlashCommandT, name: str, description: str, /, *, default: Any = <object object>, types: Optional[collections.abc.Collection[type[hikari.channels.PartialChannel]]] = None, pass_as_kwarg: bool = True ) -> ~_SlashCommandT:
View Source
    def add_channel_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        types: typing.Optional[collections.Collection[type[hikari.PartialChannel]]] = None,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a channel option to a slash command.

        .. note::
            This will always result in `hikari.InteractionChannel`.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Parameters
        ----------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        types : typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]]
            A collection of the channel classes this option should accept.

            If left as `None` or empty then the option will allow all channel types.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
            * If an invalid type is passed in `types`.
        """
        import itertools

        if types:
            try:
                channel_types = list(set(itertools.chain.from_iterable(map(_channel_types.__getitem__, types))))

            except KeyError as exc:
                raise ValueError(f"Unknown channel type {exc.args[0]}") from exc

        else:
            channel_types = None

        return self._add_option(
            name,
            description,
            hikari.OptionType.CHANNEL,
            channel_types=channel_types,
            default=default,
            pass_as_kwarg=pass_as_kwarg,
        )

Add a channel option to a slash command.

Note: This will always result in hikari.InteractionChannel.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Parameters
  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • types (typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]]): A collection of the channel classes this option should accept.

    If left as None or empty then the option will allow all channel types.

  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the command already has 25 options.
    • If an invalid type is passed in types.
#   def add_role_option( self: ~_SlashCommandT, name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> ~_SlashCommandT:
View Source
    def add_role_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a role option to a slash command.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(name, description, hikari.OptionType.ROLE, default=default, pass_as_kwarg=pass_as_kwarg)

Add a role option to a slash command.

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the command already has 25 options.
#   def add_mentionable_option( self: ~_SlashCommandT, name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> ~_SlashCommandT:
View Source
    def add_mentionable_option(
        self: _SlashCommandT,
        name: str,
        description: str,
        /,
        *,
        default: typing.Any = _UNDEFINED_DEFAULT,
        pass_as_kwarg: bool = True,
    ) -> _SlashCommandT:
        r"""Add a mentionable option to a slash command.

        .. note::
            This may target roles, guild members or users and results in
            `Union[hikari.User, hikari.InteractionMember, hikari.Role]`.

        Parameters
        ----------
        name : str
            The option's name.

            This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase.
        description : str
            The option's description.
            This should be inclusively between 1-100 characters in length.

        Other Parameters
        ----------------
        default : typing.Any
            The option's default value.
            If this is left as undefined then this option will be required.
        pass_as_kwarg : bool
            Whether or not to pass this option as a keyword argument to the
            command callback.

            Defaults to `True`. If `False` is passed here then `default` will
            only decide whether the option is required without the actual value
            being used.

        Returns
        -------
        Self
            The command object for chaining.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the option name has uppercase characters.
            * If the option description is over 100 characters in length.
            * If the command already has 25 options.
        """
        return self._add_option(
            name, description, hikari.OptionType.MENTIONABLE, default=default, pass_as_kwarg=pass_as_kwarg
        )

Add a mentionable option to a slash command.

Note: This may target roles, guild members or users and results in Union[hikari.User, hikari.InteractionMember, hikari.Role].

Parameters
  • name (str): The option's name.

    This must match the regex ^[\w-]{1,32} in Unicode mode and be lowercase.

  • description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
  • default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
  • pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.

    Defaults to True. If False is passed here then default will only decide whether the option is required without the actual value being used.

Returns
  • Self: The command object for chaining.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the option name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the option name has uppercase characters.
    • If the option description is over 100 characters in length.
    • If the command already has 25 options.
#   async def execute( self, ctx: tanjun.abc.SlashContext, /, option: Optional[hikari.interactions.command_interactions.CommandInteractionOption] = None, *, hooks: Optional[collections.abc.MutableSet[tanjun.abc.Hooks[tanjun.abc.SlashContext]]] = None ) -> None:
View Source
    async def execute(
        self,
        ctx: abc.SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        if self._always_defer and not ctx.has_been_deferred and not ctx.has_responded:
            await ctx.defer()

        ctx = ctx.set_command(self)
        own_hooks = self._hooks or _EMPTY_HOOKS
        try:
            await own_hooks.trigger_pre_execution(ctx, hooks=hooks)

            if self._tracked_options:
                kwargs = await self._process_args(ctx)

            else:
                kwargs = _EMPTY_DICT

            await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)

        except errors.CommandError as exc:
            await ctx.respond(exc.message)

        except errors.HaltExecution:
            # Unlike a message command, this won't necessarily reach the client level try except
            # block so we have to handle this here.
            await ctx.mark_not_found()

        except Exception as exc:
            if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0:
                raise

        else:
            await own_hooks.trigger_success(ctx, hooks=hooks)

        finally:
            await own_hooks.trigger_post_execution(ctx, hooks=hooks)
#   def copy( self: ~_SlashCommandT, *, _new: bool = True, parent: Optional[tanjun.abc.SlashCommandGroup] = None ) -> ~_SlashCommandT:
View Source
    def copy(
        self: _SlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _SlashCommandT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._callback = copy.copy(self._callback)
            return super().copy(_new=_new, parent=parent)

        return super().copy(_new=_new, parent=parent)

Create a copy of this command.

Returns
  • Self: A copy of this command.
View Source
class SlashCommandGroup(BaseSlashCommand, abc.SlashCommandGroup):
    """Standard implementation of a slash command group.

    .. note::
        Unlike message command grups, slash command groups cannot
        be callable functions themselves.
    """

    __slots__ = ("_commands", "_default_permission")

    def __init__(
        self,
        name: str,
        description: str,
        /,
        *,
        default_to_ephemeral: typing.Optional[bool] = None,
        default_permission: bool = True,
        is_global: bool = True,
    ) -> None:
        r"""Initialise a slash command group.

        .. note::
            Under the standard implementation, `is_global` is used to determine
            whether the command should be bulk set by `tanjun.Client.set_global_commands`
            or when `set_global_commands` is True

        Parameters
        ----------
        name : str
            The name of the command group.

            This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase.
        description : str
            The description of the command group.

        Other Parameters
        ----------------
        default_permission : bool
            Whether this command can be accessed without set permissions.

            Defaults to `True`, meaning that users can access the command by default.
        default_to_ephemeral : typing.Optional[bool]
            Whether this command's responses should default to ephemeral unless flags
            are set to override this.

            If this is left as `None` then the default set on the parent command(s),
            component or client will be in effect.
        is_global : bool
            Whether this command is a global command. Defaults to `True`.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the command name has uppercase characters.
            * If the description is over 100 characters long.
        """
        super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global)
        self._commands: dict[str, abc.BaseSlashCommand] = {}
        self._default_permission = default_permission

    @property
    def commands(self) -> collections.Collection[abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.SlashCommandGroup>>.
        return self._commands.copy().values()

    def build(self) -> special_endpoints_api.CommandBuilder:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        builder = _CommandBuilder(self._name, self._description, False).set_default_permission(self._default_permission)
        for command in self._commands.values():
            option_type = (
                hikari.OptionType.SUB_COMMAND_GROUP
                if isinstance(command, abc.SlashCommandGroup)
                else hikari.OptionType.SUB_COMMAND
            )
            command_builder = command.build()
            builder.add_option(
                hikari.CommandOption(
                    type=option_type,
                    name=command.name,
                    description=command_builder.description,
                    is_required=False,
                    options=command_builder.options,
                )
            )

        return builder

    def copy(
        self: _SlashCommandGroupT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _SlashCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._commands = {name: command.copy() for name, command in self._commands.items()}
            return super().copy(_new=_new, parent=parent)

        return super().copy(_new=_new, parent=parent)

    def add_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT:
        """Add a slash command to this group.

        .. warning::
            Command groups are only supported within top-level groups.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to add to this group.

        Returns
        -------
        Self
            Object of this group to enable chained calls.
        """
        if self._parent and isinstance(command, abc.SlashCommandGroup):
            raise ValueError("Cannot add a slash command group to a nested slash command group")

        if len(self._commands) == 25:
            raise ValueError("Cannot add more than 25 commands to a slash command group")

        if command.name in self._commands:
            raise ValueError(f"Command with name {command.name!r} already exists in this group")

        command.set_parent(self)
        self._commands[command.name] = command
        return self

    def remove_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT:
        """Remove a command from this group.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to remove from this group.

        Returns
        -------
        Self
            Object of this group to enable chained calls.
        """
        del self._commands[command.name]
        return self

    def with_command(self, command: abc.BaseSlashCommandT, /) -> abc.BaseSlashCommandT:
        """Add a slash command to this group through a decorator call.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to add to this group.

        Returns
        -------
        tanjun.abc.BaseSlashCommand
            Command which was added to this group.
        """
        self.add_command(command)
        return command

    async def execute(
        self,
        ctx: abc.SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        if not option and ctx.interaction.options:
            option = ctx.interaction.options[0]

        elif option and option.options:
            option = option.options[0]

        else:
            raise RuntimeError("Missing sub-command option")

        if command := self._commands.get(option.name):
            if command.defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(command.defaults_to_ephemeral)

            if await command.check_context(ctx):
                await command.execute(ctx, option=option, hooks=hooks)
                return

        await ctx.mark_not_found()

Standard implementation of a slash command group.

Note: Unlike message command grups, slash command groups cannot be callable functions themselves.

#   SlashCommandGroup( name: str, description: str, /, *, default_to_ephemeral: Optional[bool] = None, default_permission: bool = True, is_global: bool = True )
View Source
    def __init__(
        self,
        name: str,
        description: str,
        /,
        *,
        default_to_ephemeral: typing.Optional[bool] = None,
        default_permission: bool = True,
        is_global: bool = True,
    ) -> None:
        r"""Initialise a slash command group.

        .. note::
            Under the standard implementation, `is_global` is used to determine
            whether the command should be bulk set by `tanjun.Client.set_global_commands`
            or when `set_global_commands` is True

        Parameters
        ----------
        name : str
            The name of the command group.

            This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase.
        description : str
            The description of the command group.

        Other Parameters
        ----------------
        default_permission : bool
            Whether this command can be accessed without set permissions.

            Defaults to `True`, meaning that users can access the command by default.
        default_to_ephemeral : typing.Optional[bool]
            Whether this command's responses should default to ephemeral unless flags
            are set to override this.

            If this is left as `None` then the default set on the parent command(s),
            component or client will be in effect.
        is_global : bool
            Whether this command is a global command. Defaults to `True`.

        Raises
        ------
        ValueError
            Raises a value error for any of the following reasons:
            * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode).
            * If the command name has uppercase characters.
            * If the description is over 100 characters long.
        """
        super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global)
        self._commands: dict[str, abc.BaseSlashCommand] = {}
        self._default_permission = default_permission

Initialise a slash command group.

Note: Under the standard implementation, is_global is used to determine whether the command should be bulk set by tanjun.Client.set_global_commands or when set_global_commands is True

Parameters
  • name (str): The name of the command group.

    This must match the regex ^[\w-]{1,32}$ in Unicode mode and be lowercase.

  • description (str): The description of the command group.
Other Parameters
  • default_permission (bool): Whether this command can be accessed without set permissions.

    Defaults to True, meaning that users can access the command by default.

  • default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.

    If this is left as None then the default set on the parent command(s), component or client will be in effect.

  • is_global (bool): Whether this command is a global command. Defaults to True.
Raises
  • ValueError: Raises a value error for any of the following reasons:
    • If the command name doesn't match the regex ^[\w-]{1,32}$ (Unicode mode).
    • If the command name has uppercase characters.
    • If the description is over 100 characters long.
#   commands: collections.abc.Collection[tanjun.abc.BaseSlashCommand]

Collection of the commands in this group.

#   def build(self) -> hikari.api.special_endpoints.CommandBuilder:
View Source
    def build(self) -> special_endpoints_api.CommandBuilder:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        builder = _CommandBuilder(self._name, self._description, False).set_default_permission(self._default_permission)
        for command in self._commands.values():
            option_type = (
                hikari.OptionType.SUB_COMMAND_GROUP
                if isinstance(command, abc.SlashCommandGroup)
                else hikari.OptionType.SUB_COMMAND
            )
            command_builder = command.build()
            builder.add_option(
                hikari.CommandOption(
                    type=option_type,
                    name=command.name,
                    description=command_builder.description,
                    is_required=False,
                    options=command_builder.options,
                )
            )

        return builder

Get a builder object for this command.

Returns
  • hikari.api.CommandBuilder: A builder object for this command. Use to declare this command on globally or for a specific guild.
#   def copy( self: ~_SlashCommandGroupT, *, _new: bool = True, parent: Optional[tanjun.abc.SlashCommandGroup] = None ) -> ~_SlashCommandGroupT:
View Source
    def copy(
        self: _SlashCommandGroupT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None
    ) -> _SlashCommandGroupT:
        # <<inherited docstring from tanjun.abc.ExecutableCommand>>.
        if not _new:
            self._commands = {name: command.copy() for name, command in self._commands.items()}
            return super().copy(_new=_new, parent=parent)

        return super().copy(_new=_new, parent=parent)

Create a copy of this command.

Returns
  • Self: A copy of this command.
#   def add_command( self: ~_SlashCommandGroupT, command: tanjun.abc.BaseSlashCommand, / ) -> ~_SlashCommandGroupT:
View Source
    def add_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT:
        """Add a slash command to this group.

        .. warning::
            Command groups are only supported within top-level groups.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to add to this group.

        Returns
        -------
        Self
            Object of this group to enable chained calls.
        """
        if self._parent and isinstance(command, abc.SlashCommandGroup):
            raise ValueError("Cannot add a slash command group to a nested slash command group")

        if len(self._commands) == 25:
            raise ValueError("Cannot add more than 25 commands to a slash command group")

        if command.name in self._commands:
            raise ValueError(f"Command with name {command.name!r} already exists in this group")

        command.set_parent(self)
        self._commands[command.name] = command
        return self

Add a slash command to this group.

Warning: Command groups are only supported within top-level groups.

Parameters
Returns
  • Self: Object of this group to enable chained calls.
#   def remove_command( self: ~_SlashCommandGroupT, command: tanjun.abc.BaseSlashCommand, / ) -> ~_SlashCommandGroupT:
View Source
    def remove_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT:
        """Remove a command from this group.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to remove from this group.

        Returns
        -------
        Self
            Object of this group to enable chained calls.
        """
        del self._commands[command.name]
        return self

Remove a command from this group.

Parameters
Returns
  • Self: Object of this group to enable chained calls.
#   def with_command(self, command: ~BaseSlashCommandT, /) -> ~BaseSlashCommandT:
View Source
    def with_command(self, command: abc.BaseSlashCommandT, /) -> abc.BaseSlashCommandT:
        """Add a slash command to this group through a decorator call.

        Parameters
        ----------
        command : tanjun.abc.BaseSlashCommand
            Command to add to this group.

        Returns
        -------
        tanjun.abc.BaseSlashCommand
            Command which was added to this group.
        """
        self.add_command(command)
        return command

Add a slash command to this group through a decorator call.

Parameters
Returns
#   async def execute( self, ctx: tanjun.abc.SlashContext, /, option: Optional[hikari.interactions.command_interactions.CommandInteractionOption] = None, *, hooks: Optional[collections.abc.MutableSet[tanjun.abc.Hooks[tanjun.abc.SlashContext]]] = None ) -> None:
View Source
    async def execute(
        self,
        ctx: abc.SlashContext,
        /,
        option: typing.Optional[hikari.CommandInteractionOption] = None,
        *,
        hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.BaseSlashCommand>>.
        if not option and ctx.interaction.options:
            option = ctx.interaction.options[0]

        elif option and option.options:
            option = option.options[0]

        else:
            raise RuntimeError("Missing sub-command option")

        if command := self._commands.get(option.name):
            if command.defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(command.defaults_to_ephemeral)

            if await command.check_context(ctx):
                await command.execute(ctx, option=option, hooks=hooks)
                return

        await ctx.mark_not_found()
#   def with_str_slash_option( name: str, description: str, /, *, choices: Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], NoneType] = None, converters: Union[collections.abc.Sequence[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any = <object object>, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_str_slash_option(
    name: str,
    description: str,
    /,
    *,
    choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None,
    converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (),
    default: typing.Any = _UNDEFINED_DEFAULT,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a string option to a slash command.

    For more information on this function's parameters see `SlashCommand.add_str_option`.

    Examples
    --------
    ```py
    @with_str_slash_option("name", "A name.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, name: str) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_str_option(
        name,
        description,
        default=default,
        choices=choices,
        converters=converters,
        pass_as_kwarg=pass_as_kwarg,
        _stack_level=1,
    )

Add a string option to a slash command.

For more information on this function's parameters see SlashCommand.add_str_option.

Examples
@with_str_slash_option("name", "A name.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, name: str) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_int_slash_option( name: str, description: str, /, *, choices: Optional[collections.abc.Mapping[str, int]] = None, converters: Union[collections.abc.Collection[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any = <object object>, min_value: Optional[int] = None, max_value: Optional[int] = None, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_int_slash_option(
    name: str,
    description: str,
    /,
    *,
    choices: typing.Optional[collections.Mapping[str, int]] = None,
    converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
    default: typing.Any = _UNDEFINED_DEFAULT,
    min_value: typing.Optional[int] = None,
    max_value: typing.Optional[int] = None,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add an integer option to a slash command.

    For information on this function's parameters see `SlashCommand.add_int_option`.

    Examples
    --------
    ```py
    @with_int_slash_option("int_value", "Int value.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, int_value: int) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_int_option(
        name,
        description,
        default=default,
        choices=choices,
        converters=converters,
        min_value=min_value,
        max_value=max_value,
        pass_as_kwarg=pass_as_kwarg,
        _stack_level=1,
    )

Add an integer option to a slash command.

For information on this function's parameters see SlashCommand.add_int_option.

Examples
@with_int_slash_option("int_value", "Int value.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, int_value: int) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_float_slash_option( name: str, description: str, /, *, always_float: bool = True, choices: Optional[collections.abc.Mapping[str, float]] = None, converters: Union[collections.abc.Collection[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any = <object object>, min_value: Optional[float] = None, max_value: Optional[float] = None, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_float_slash_option(
    name: str,
    description: str,
    /,
    *,
    always_float: bool = True,
    choices: typing.Optional[collections.Mapping[str, float]] = None,
    converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (),
    default: typing.Any = _UNDEFINED_DEFAULT,
    min_value: typing.Optional[float] = None,
    max_value: typing.Optional[float] = None,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a float option to a slash command.

    For information on this function's parameters see `SlashCommand.add_float_option`.

    Examples
    --------
    ```py
    @with_float_slash_option("float_value", "Float value.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, float_value: float) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_float_option(
        name,
        description,
        always_float=always_float,
        default=default,
        choices=choices,
        converters=converters,
        min_value=min_value,
        max_value=max_value,
        pass_as_kwarg=pass_as_kwarg,
        _stack_level=1,
    )

Add a float option to a slash command.

For information on this function's parameters see SlashCommand.add_float_option.

Examples
@with_float_slash_option("float_value", "Float value.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, float_value: float) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_bool_slash_option( name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_bool_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a boolean option to a slash command.

    For information on this function's parameters see `SlashContext.add_bool_option`.

    Examples
    --------
    ```py
    @with_bool_slash_option("flag", "Whether this flag should be enabled.", default=False)
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, flag: bool) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_bool_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)

Add a boolean option to a slash command.

For information on this function's parameters see SlashContext.add_bool_option.

Examples
@with_bool_slash_option("flag", "Whether this flag should be enabled.", default=False)
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, flag: bool) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_role_slash_option( name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_role_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a role option to a slash command.

    For information on this function's parameters see `SlashCommand.add_role_option`.

    Examples
    --------
    ```py
    @with_role_slash_option("role", "Role to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, role: hikari.Role) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_role_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)

Add a role option to a slash command.

For information on this function's parameters see SlashCommand.add_role_option.

Examples
@with_role_slash_option("role", "Role to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, role: hikari.Role) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_user_slash_option( name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_user_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a user option to a slash command.

    For information on this function's parameters see `SlashContext.add_user_option`.

    .. note::
        This may result in `hikari.InteractionMember` or
        `hikari.users.User` if the user isn't in the current guild or if this
        command was executed in a DM channel.

    Examples
    --------
    ```py
    @with_user_slash_option("user", "user to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, user: Union[InteractionMember, User]) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_user_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)

Add a user option to a slash command.

For information on this function's parameters see SlashContext.add_user_option.

Note: This may result in hikari.InteractionMember or hikari.users.User if the user isn't in the current guild or if this command was executed in a DM channel.

Examples
@with_user_slash_option("user", "user to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, user: Union[InteractionMember, User]) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_member_slash_option( name: str, description: str, /, *, default: Any = <object object> ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_member_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a member option to a slash command.

    For information on this function's arguments see `SlashCommand.add_member_option`.

    .. note::
        This will always result in `hikari.InteractionMember`.

    Examples
    --------
    ```py
    @with_member_slash_option("member", "member to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, member: hikari.InteractionMember) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_member_option(name, description, default=default)

Add a member option to a slash command.

For information on this function's arguments see SlashCommand.add_member_option.

Note: This will always result in hikari.InteractionMember.

Examples
@with_member_slash_option("member", "member to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, member: hikari.InteractionMember) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_channel_slash_option( name: str, description: str, /, *, types: Optional[collections.abc.Collection[type[hikari.channels.PartialChannel]]] = None, default: Any = <object object>, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_channel_slash_option(
    name: str,
    description: str,
    /,
    *,
    types: typing.Union[collections.Collection[type[hikari.PartialChannel]], None] = None,
    default: typing.Any = _UNDEFINED_DEFAULT,
    pass_as_kwarg: bool = True,
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a channel option to a slash command.

    For information on this function's parameters see `SlashCommand.add_channel_option`.

    .. note::
        This will always result in `hikari..InteractionChannel`.

    Examples
    --------
    ```py
    @with_channel_slash_option("channel", "channel to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, channel: hikari.InteractionChannel) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_channel_option(name, description, types=types, default=default, pass_as_kwarg=pass_as_kwarg)

Add a channel option to a slash command.

For information on this function's parameters see SlashCommand.add_channel_option.

Note: This will always result in hikari..InteractionChannel.

Examples
@with_channel_slash_option("channel", "channel to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, channel: hikari.InteractionChannel) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
#   def with_mentionable_slash_option( name: str, description: str, /, *, default: Any = <object object>, pass_as_kwarg: bool = True ) -> collections.abc.Callable[[~_SlashCommandT], ~_SlashCommandT]:
View Source
def with_mentionable_slash_option(
    name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True
) -> collections.Callable[[_SlashCommandT], _SlashCommandT]:
    """Add a mentionable option to a slash command.

    For information on this function's arguments see `SlashCommand.add_mentionable_option`.

    .. note::
        This may target roles, guild members or users and results in
        `Union[hikari.User, hikari.InteractionMember, hikari.Role]`.

    Examples
    --------
    ```py
    @with_mentionable_slash_option("mentionable", "Mentionable entity to target.")
    @as_slash_command("command", "A command")
    async def command(self, ctx: tanjun.abc.SlashContext, mentionable: [Role, InteractionMember, User]) -> None:
        ...
    ```

    Returns
    -------
    collections.abc.Callable[[_SlashCommandT], _SlashCommandT]
        Decorator callback which adds the option to the command.
    """
    return lambda c: c.add_mentionable_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)

Add a mentionable option to a slash command.

For information on this function's arguments see SlashCommand.add_mentionable_option.

Note: This may target roles, guild members or users and results in Union[hikari.User, hikari.InteractionMember, hikari.Role].

Examples
@with_mentionable_slash_option("mentionable", "Mentionable entity to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, mentionable: [Role, InteractionMember, User]) -> None:
    ...
Returns
  • collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Standard implementation of Tanjun's "components" used to manage separate features within a client."""
from __future__ import annotations

__all__: list[str] = [
    "CommandT",
    "Component",
    "AbstractComponentLoader",
    "OnCallbackSig",
    "OnCallbackSigT",
    "WithCommandReturnSig",
]

import abc
import asyncio
import base64
import copy
import inspect
import itertools
import logging
import random
import typing
from collections import abc as collections

from . import abc as tanjun_abc
from . import checks as checks_
from . import errors
from . import injecting
from . import utilities

if typing.TYPE_CHECKING:
    from hikari.events import base_events

    from . import schedules

    _ComponentT = typing.TypeVar("_ComponentT", bound="Component")
    _ScheduleT = typing.TypeVar("_ScheduleT", bound=schedules.AbstractSchedule)


CommandT = typing.TypeVar("CommandT", bound="tanjun_abc.ExecutableCommand[typing.Any]")
_LOGGER = logging.getLogger("hikari.tanjun.components")
# This errors on earlier 3.9 releases when not quotes cause dumb handling of the [CommandT] list
WithCommandReturnSig = typing.Union[CommandT, "collections.Callable[[CommandT], CommandT]"]

OnCallbackSig = collections.Callable[..., tanjun_abc.MaybeAwaitableT[None]]
"""Type hint of a on_open or on_close component callback.

These support dependency injection, should expect no positional arguments and
should return `None`.
"""


OnCallbackSigT = typing.TypeVar("OnCallbackSigT", bound=OnCallbackSig)
"""Generic version of `OnCallbackSig`."""


class AbstractComponentLoader(abc.ABC):
    """Abstract interface used for loading utility into a standard `Component`."""

    __slots__ = ()

    @abc.abstractmethod
    def load_into_component(self, component: tanjun_abc.Component, /) -> None:
        """Load the object into the component.

        Parameters
        ----------
        component : tanjun.abc.Component
            The component this object should be loaded into.
        """


def _with_command(
    add_command: collections.Callable[[CommandT], Component],
    maybe_command: typing.Optional[CommandT],
    /,
    *,
    copy: bool = False,
) -> WithCommandReturnSig[CommandT]:
    if maybe_command:
        maybe_command = maybe_command.copy() if copy else maybe_command
        add_command(maybe_command)
        return maybe_command

    def decorator(command: CommandT, /) -> CommandT:
        command = command.copy() if copy else command
        add_command(command)
        return command

    return decorator


def _filter_scope(scope: collections.Mapping[str, typing.Any]) -> collections.Iterator[typing.Any]:
    return (value for key, value in scope.items() if not key.startswith("_"))


class _ComponentManager(tanjun_abc.ClientLoader):
    __slots__ = ("_component", "_copy")

    def __init__(self, component: Component, copy: bool) -> None:
        self._component = component
        self._copy = copy

    @property
    def has_load(self) -> bool:
        return True

    @property
    def has_unload(self) -> bool:
        return True

    def load(self, client: tanjun_abc.Client, /) -> bool:
        client.add_component(self._component.copy() if self._copy else self._component)
        return True

    def unload(self, client: tanjun_abc.Client, /) -> bool:
        client.remove_component_by_name(self._component.name)
        return True


# TODO: do we want to setup a custom equality and hash here to make it easier to unload components?
class Component(tanjun_abc.Component):
    """Standard implementation of `tanjun.abc.Component`.

    This is a collcetion of commands (both message and slash), hooks and listener
    callbacks which can be added to a generic client.

    .. note::
        This implementation supports dependency injection for its checks,
        command callbacks and listeners when linked to a client which
        supports dependency injection.
    """

    __slots__ = (
        "_checks",
        "_client",
        "_client_callbacks",
        "_defaults_to_ephemeral",
        "_hooks",
        "_is_strict",
        "_listeners",
        "_loop",
        "_message_commands",
        "_message_hooks",
        "_metadata",
        "_name",
        "_names_to_commands",
        "_on_close",
        "_on_open",
        "_schedules",
        "_slash_commands",
        "_slash_hooks",
    )

    def __init__(self, *, name: typing.Optional[str] = None, strict: bool = False) -> None:
        """Initialise a new component.

        Other Parameters
        ----------------
        name : str
            The component's identifier.

            If not provided then this will be a random string.
        strict : bool
            Whether this component should use a stricter (more optimal) approach
            for message command search.

            When this is `True`, message command names will not be allowed to contain
            spaces and will have to be unique to one command within the component.
        """
        self._checks: list[checks_.InjectableCheck] = []
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._client_callbacks: dict[str, list[tanjun_abc.MetaEventSig]] = {}
        self._defaults_to_ephemeral: typing.Optional[bool] = None
        self._hooks: typing.Optional[tanjun_abc.AnyHooks] = None
        self._is_strict = strict
        self._listeners: dict[type[base_events.Event], list[tanjun_abc.ListenerCallbackSig]] = {}
        self._loop: typing.Optional[asyncio.AbstractEventLoop] = None
        self._message_commands: list[tanjun_abc.MessageCommand[typing.Any]] = []
        self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None
        self._metadata: dict[typing.Any, typing.Any] = {}
        self._name = name or base64.b64encode(random.randbytes(32)).decode()
        self._names_to_commands: dict[str, tanjun_abc.MessageCommand[typing.Any]] = {}
        self._on_close: list[injecting.CallbackDescriptor[None]] = []
        self._on_open: list[injecting.CallbackDescriptor[None]] = []
        self._schedules: list[schedules.AbstractSchedule] = []
        self._slash_commands: dict[str, tanjun_abc.BaseSlashCommand] = {}
        self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None

    def __repr__(self) -> str:
        return f"{type(self).__name__}({self.checks=}, {self.hooks=}, {self.slash_hooks=}, {self.message_hooks=})"

    @property
    def checks(self) -> collections.Collection[tanjun_abc.CheckSig]:
        """Collection of the checks being run against every command execution in this component."""
        return tuple(check.callback for check in self._checks)

    @property
    def client(self) -> typing.Optional[tanjun_abc.Client]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._client

    @property
    def defaults_to_ephemeral(self) -> typing.Optional[bool]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._defaults_to_ephemeral

    @property
    def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._hooks

    @property
    def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._loop

    @property
    def name(self) -> str:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._name

    @property
    def schedules(self) -> collections.Collection[schedules.AbstractSchedule]:
        """Collection of the schedules registered to this component."""
        return self._schedules

    @property
    def slash_commands(self) -> collections.Collection[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._slash_commands.copy().values()

    @property
    def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._slash_hooks

    @property
    def message_commands(self) -> collections.Collection[tanjun_abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._message_commands.copy()

    @property
    def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._message_hooks

    @property
    def needs_injector(self) -> bool:
        """Whether any of the checks in this component require dependency injection."""
        return any(check.needs_injector for check in self._checks)

    @property
    def listeners(
        self,
    ) -> collections.Mapping[type[base_events.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return utilities.CastedView(self._listeners, lambda x: x.copy())

    @property
    def metadata(self) -> dict[typing.Any, typing.Any]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._metadata

    def copy(self: _ComponentT, *, _new: bool = True) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not _new:
            self._checks = [check.copy() for check in self._checks]
            self._slash_commands = {name: command.copy() for name, command in self._slash_commands.items()}
            self._hooks = self._hooks.copy() if self._hooks else None
            self._listeners = {
                event: [copy.copy(listener) for listener in listeners] for event, listeners in self._listeners.items()
            }
            commands = {command: command.copy() for command in self._message_commands}
            self._message_commands = list(commands.values())
            self._metadata = self._metadata.copy()
            self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()}
            self._schedules = [schedule.copy() for schedule in self._schedules] if self._schedules else []
            return self

        return copy.copy(self).copy(_new=False)

    @typing.overload
    def load_from_scope(
        self: _ComponentT, *, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None
    ) -> _ComponentT:
        ...

    @typing.overload
    def load_from_scope(self: _ComponentT, *, include_globals: bool = False) -> _ComponentT:
        ...

    def load_from_scope(
        self: _ComponentT,
        *,
        include_globals: bool = False,
        scope: typing.Optional[collections.Mapping[str, typing.Any]] = None,
    ) -> _ComponentT:
        """Load entries such as top-level commands into the component from the calling scope.

        Notes
        -----
        * This will load schedules which support `AbstractComponentLoader`
          (e.g. `tanjun.schedules.IntervalSchedule`).
        * This will ignore commands which are owned by command groups.
        * This will detect entries from the calling scope which implement
          `AbstractComponentLoader` unless `scope` is passed but this isn't possible
          in a stack-less python implementation; in stack-less environments the
          scope will have to be explicitly passed as `scope`.

        Other Parameters
        ----------------
        include_globals: bool
            Whether to include global variables (along with local) while
            detecting from the calling scope.

            This defaults to `False`, cannot be `True` when `scope` is provided
            and will only ever be needed when the local scope is different
            from the global scope.
        scope : typing.Optional[collections.Mapping[str, typing.Any]]
            The scope to detect entries which implement `AbstractComponentLoader`
            from.

            This overrides the default usage of stackframe introspection.

        Returns
        -------
        Self
            The current component to allow for chaining.

        Raises
        ------
        RuntimeError
            If this is called in a python implementation which doesn't support
            stack frame inspection when `scope` is not provided.
        ValueError
            If `scope` is provided when `include_globals` is True.
        """
        if scope is None:
            if not (stack := inspect.currentframe()) or not stack.f_back:
                raise RuntimeError(
                    "Stackframe introspection is not supported in this runtime. Please explicitly pass `scope`."
                )

            values_iter = _filter_scope(stack.f_back.f_locals)
            if include_globals:
                values_iter = itertools.chain(values_iter, _filter_scope(stack.f_back.f_globals))

        elif include_globals:
            raise ValueError("Cannot specify include_globals as True when scope is passed")

        else:
            values_iter = _filter_scope(scope)

        _LOGGER.info(
            "Loading commands for %s component from %s parent scope(s)",
            self.name,
            "global and local" if include_globals else "local",
        )
        for value in values_iter:
            if isinstance(value, AbstractComponentLoader):
                value.load_into_component(self)

        return self

    def set_ephemeral_default(self: _ComponentT, state: typing.Optional[bool], /) -> _ComponentT:
        """Set whether slash contexts executed in this component should default to ephemeral responses.

        Parameters
        ----------
        typing.Optional[bool]
            Whether slash command contexts executed in this component should
            should default to ephemeral.
            This will be overridden by any response calls which specify flags.

            Setting this to `None` will let the default set on the parent
            client propagate and decide the ephemeral default behaviour.

        Returns
        -------
        SelfT
            This component to enable method chaining.
        """
        self._defaults_to_ephemeral = state
        return self

    def set_metadata(self: _ComponentT, key: typing.Any, value: typing.Any, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._metadata[key] = value
        return self

    def set_slash_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.SlashHooks], /) -> _ComponentT:
        self._slash_hooks = hooks_
        return self

    def set_message_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.MessageHooks], /) -> _ComponentT:
        self._message_hooks = hooks_
        return self

    def set_hooks(self: _ComponentT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ComponentT:
        self._hooks = hooks
        return self

    def add_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT:
        if check not in self._checks:
            self._checks.append(checks_.InjectableCheck(check))

        return self

    def remove_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT:
        self._checks.remove(typing.cast("checks_.InjectableCheck", check))
        return self

    def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT:
        self.add_check(check)
        return check

    def add_client_callback(
        self: _ComponentT,
        event_name: typing.Union[str, tanjun_abc.ClientCallbackNames],
        callback: tanjun_abc.MetaEventSig,
        /,
    ) -> _ComponentT:
        event_name = event_name.lower()
        try:
            if callback in self._client_callbacks[event_name]:
                return self

            self._client_callbacks[event_name].append(callback)
        except KeyError:
            self._client_callbacks[event_name] = [callback]

        if self._client:
            self._client.add_client_callback(event_name, callback)

        return self

    def get_client_callbacks(
        self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Collection[tanjun_abc.MetaEventSig]:
        event_name = event_name.lower()
        return self._client_callbacks.get(event_name) or ()

    def remove_client_callback(self, event_name: str, callback: tanjun_abc.MetaEventSig, /) -> None:
        event_name = event_name.lower()
        self._client_callbacks[event_name].remove(callback)
        if not self._client_callbacks[event_name]:
            del self._client_callbacks[event_name]

        if self._client:
            self._client.remove_client_callback(event_name, callback)

    def with_client_callback(
        self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]:
        def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT:
            self.add_client_callback(event_name, callback)
            return callback

        return decorator

    def add_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT:
        """Add a command to this component.

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to add.

        Returns
        -------
        Self
            The current component to allow for chaining.
        """
        if isinstance(command, tanjun_abc.MessageCommand):
            self.add_message_command(command)

        elif isinstance(command, tanjun_abc.BaseSlashCommand):
            self.add_slash_command(command)

        else:
            raise ValueError(
                f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}"
            )

        return self

    def remove_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT:
        """Remove a command from this component.

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to remove.

        Returns
        -------
        Self
            This component to enable method chaining.
        """
        if isinstance(command, tanjun_abc.MessageCommand):
            self.remove_message_command(command)

        elif isinstance(command, tanjun_abc.BaseSlashCommand):
            self.remove_slash_command(command)

        else:
            raise ValueError(
                f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}"
            )

        return self

    @typing.overload
    def with_command(self, command: CommandT, /) -> CommandT:
        ...

    @typing.overload
    def with_command(self, /, *, copy: bool = False) -> collections.Callable[[CommandT], CommandT]:
        ...

    def with_command(
        self, command: typing.Optional[CommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[CommandT]:
        """Add a command to this component through a decorator call.

        Examples
        --------
        This may be used inconjunction with `tanjun.as_slash_command`
        and `tanjun.as_message_command`.

        ```py
        @component.with_command
        @tanjun.with_slash_str_option("option_name", "option description")
        @tanjun.as_slash_command("command_name", "command description")
        async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None:
            await ctx.respond(f"Hi {arg}")
        ```

        ```py
        @component.with_command
        @tanjun.with_argument("argument_name")
        @tanjun.as_message_command("command_name")
        async def message_command(ctx: tanjun.abc.Context, arg: str) -> None:
            await ctx.respond(f"Hi {arg}")
        ```

        Parameters
        ----------
        command CommandT
            The command to add to this component.

        Other Parameters
        ----------------
        copy : bool
            Whether to copy the command before adding it to this component.

        Returns
        -------
        CommandT
            The added command.
        """
        return _with_command(self.add_command, command, copy=copy)

    def add_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._slash_commands.get(command.name) == command:
            return self

        command.bind_component(self)

        if self._client:
            command.bind_client(self._client)

        self._slash_commands[command.name.casefold()] = command
        return self

    def remove_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        try:
            del self._slash_commands[command.name.casefold()]
        except KeyError:
            raise ValueError(f"Command {command.name} not found") from None

        return self

    @typing.overload
    def with_slash_command(self, command: tanjun_abc.BaseSlashCommandT, /) -> tanjun_abc.BaseSlashCommandT:
        ...

    @typing.overload
    def with_slash_command(
        self, /, *, copy: bool = False
    ) -> collections.Callable[[tanjun_abc.BaseSlashCommandT], tanjun_abc.BaseSlashCommandT]:
        ...

    def with_slash_command(
        self, command: typing.Optional[tanjun_abc.BaseSlashCommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[tanjun_abc.BaseSlashCommandT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return _with_command(self.add_slash_command, command, copy=copy)

    def add_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT:
        """Add a message command to the component.

        Parameters
        ----------
        command : tanjun.abc.MessageCommand
            The command to add.

        Returns
        -------
        Self
            The component to allow method chaining.

        Raises
        ------
        ValueError
            If one of the command's name is already registered in a strict
            component.
        """
        if command in self._message_commands:
            return self

        if self._is_strict:
            if any(" " in name for name in command.names):
                raise ValueError("Command name cannot contain spaces for this component implementation")

            if name_conflicts := self._names_to_commands.keys() & command.names:
                raise ValueError(
                    "Sub-command names must be unique in a strict component. "
                    "The following conflicts were found " + ", ".join(name_conflicts)
                )

            self._names_to_commands.update((name, command) for name in command.names)

        self._message_commands.append(command)

        if self._client:
            command.bind_client(self._client)

        command.bind_component(self)
        return self

    def remove_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._message_commands.remove(command)

        if self._is_strict:
            for name in command.names:
                if self._names_to_commands.get(name) == command:
                    del self._names_to_commands[name]

        return self

    @typing.overload
    def with_message_command(self, command: tanjun_abc.MessageCommandT, /) -> tanjun_abc.MessageCommandT:
        ...

    @typing.overload
    def with_message_command(
        self, /, *, copy: bool = False
    ) -> collections.Callable[[tanjun_abc.MessageCommandT], tanjun_abc.MessageCommandT]:
        ...

    def with_message_command(
        self, command: typing.Optional[tanjun_abc.MessageCommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[tanjun_abc.MessageCommandT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return _with_command(self.add_message_command, command, copy=copy)

    def add_listener(
        self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, /
    ) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        try:
            if listener in self._listeners[event]:
                return self

            self._listeners[event].append(listener)

        except KeyError:
            self._listeners[event] = [listener]

        if self._client:
            self._client.add_listener(event, listener)

        return self

    def remove_listener(
        self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, /
    ) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._listeners[event].remove(listener)
        if not self._listeners[event]:
            del self._listeners[event]

        if self._client:
            self._client.remove_listener(event, listener)

        return self

    # TODO: make event optional?
    def with_listener(
        self, event_type: type[base_events.Event]
    ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        def decorator(callback: tanjun_abc.ListenerCallbackSigT) -> tanjun_abc.ListenerCallbackSigT:
            self.add_listener(event_type, callback)
            return callback

        return decorator

    def add_on_close(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT:
        """Add a close callback to this component.

        .. note::
            Unlike the closing and closed client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The close callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        Self
            The component object to enable call chaining.
        """
        self._on_close.append(injecting.CallbackDescriptor(callback))
        return self

    def with_on_close(self, callback: OnCallbackSigT, /) -> OnCallbackSigT:
        """Add a close callback to this component through a decorator call.

        .. note::
            Unlike the closing and closed client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The close callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        OnCallbackSig
            The added close callback.
        """
        self.add_on_close(callback)
        return callback

    def add_on_open(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT:
        """Add a open callback to this component.

        .. note::
            Unlike the starting and started client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The open callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        Self
            The component object to enable call chaining.
        """
        self._on_open.append(injecting.CallbackDescriptor(callback))
        return self

    def with_on_open(self, callback: OnCallbackSigT, /) -> OnCallbackSigT:
        """Add a open callback to this component through a decorator call.

        .. note::
            Unlike the starting and started client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The open callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        OnCallbackSig
            The added open callback.
        """
        self.add_on_open(callback)
        return callback

    def bind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._client:
            raise RuntimeError("Client already set")

        self._client = client
        for message_command in self._message_commands:
            message_command.bind_client(client)

        for slash_command in self._slash_commands.values():
            slash_command.bind_client(client)

        for event, listeners in self._listeners.items():
            for listener in listeners:
                self._client.add_listener(event, listener)

        for event_name, callbacks in self._client_callbacks.items():
            for callback in callbacks:
                self._client.add_client_callback(event_name, callback)

        return self

    def unbind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not self._client or self._client != client:
            raise RuntimeError("Component isn't bound to this client")

        for event, listeners in self._listeners.items():
            for listener in listeners:
                try:
                    self._client.remove_listener(event, listener)
                except (LookupError, ValueError):
                    pass

        for event_name, callbacks in self._client_callbacks.items():
            for callback in callbacks:
                try:
                    self._client.remove_client_callback(event_name, callback)
                except (LookupError, ValueError):
                    pass

        self._client = None

        return self

    async def _check_context(self, ctx: tanjun_abc.Context, /) -> bool:
        return await utilities.gather_checks(ctx, self._checks)

    async def _check_message_context(
        self, ctx: tanjun_abc.MessageContext, /
    ) -> collections.AsyncIterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        ctx.set_component(self)

        if self._is_strict:
            name = ctx.content.split(" ", 1)[0]
            command = self._names_to_commands.get(name)
            if command and await self._check_context(ctx) and await command.check_context(ctx):
                yield name, command

            else:
                ctx.set_component(None)

            return

        checks_run = False
        for name, command in self.check_message_name(ctx.content):
            if not checks_run:
                if not await self._check_context(ctx):
                    return

                checks_run = True

            if await command.check_context(ctx):
                yield name, command

        ctx.set_component(None)

    def check_message_name(
        self, content: str, /
    ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._is_strict:
            name = content.split(" ", 1)[0]
            if command := self._names_to_commands.get(name):
                yield name, command
            return

        for command in self._message_commands:
            if (name_ := utilities.match_prefix_names(content, command.names)) is not None:
                yield name_, command

    def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Component>>.
        if command := self._slash_commands.get(name):
            yield command

    async def _execute_interaction(
        self,
        ctx: tanjun_abc.SlashContext,
        command: typing.Optional[tanjun_abc.BaseSlashCommand],
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None,
    ) -> typing.Optional[collections.Awaitable[None]]:
        try:
            if not command or not await self._check_context(ctx) or not await command.check_context(ctx):
                return None

        except errors.HaltExecution:
            return asyncio.get_running_loop().create_task(ctx.mark_not_found())

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            asyncio.get_running_loop().create_future().set_result(None)
            return None

        if self._slash_hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._slash_hooks)

        if self._hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._hooks)

        return asyncio.get_running_loop().create_task(command.execute(ctx, hooks=hooks))

    # To ensure that ctx.set_ephemeral_default is called as soon as possible if
    # a match is found the public function is kept sync to avoid yielding
    # to the event loop until after this is set.
    def execute_interaction(
        self,
        ctx: tanjun_abc.SlashContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None,
    ) -> collections.Coroutine[typing.Any, typing.Any, typing.Optional[collections.Awaitable[None]]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        command = self._slash_commands.get(ctx.interaction.command_name)
        if command:
            if command.defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(command.defaults_to_ephemeral)

            elif self._defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(self._defaults_to_ephemeral)

        return self._execute_interaction(ctx, command, hooks=hooks)

    async def execute_message(
        self,
        ctx: tanjun_abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.MessageHooks]] = None,
    ) -> bool:
        # <<inherited docstring from tanjun.abc.Component>>.
        async for name, command in self._check_message_context(ctx):
            ctx.set_triggering_name(name)
            ctx.set_content(ctx.content[len(name) :].lstrip())
            ctx.set_component(self)
            # Only add our hooks if we're sure we'll be executing the command here.

            if self._message_hooks:
                if hooks is None:
                    hooks = set()

                hooks.add(self._message_hooks)

            if self._hooks:
                if hooks is None:
                    hooks = set()

                hooks.add(self._hooks)

            await command.execute(ctx, hooks=hooks)
            return True

        ctx.set_component(None)
        return False

    def _load_from_properties(self) -> None:
        for _, member in inspect.getmembers(self):
            if isinstance(member, AbstractComponentLoader):
                member.load_into_component(self)

    def add_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT:
        """Add a schedule to the component.

        Parameters
        ----------
        schedule : tanjun.schedules.AbstractSchedule
            The schedule to add.

        Returns
        -------
        Self
            The component itself for chaining.
        """
        if self._client and self._loop:
            # TODO: upgrade this to the standard interface
            assert isinstance(self._client, injecting.InjectorClient)
            schedule.start(self._client, loop=self._loop)

        self._schedules.append(schedule)
        return self

    def remove_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT:
        """Remove a schedule from the component.

        Parameters
        ----------
        schedule : tanjun.schedules.AbstractSchedule
            The schedule to remove

        Returns
        -------
        Self
            The component itself for chaining.

        Raises
        ------
        ValueError
            If the schedule isn't registered.
        """
        if schedule.is_alive:
            schedule.stop()

        self._schedules.remove(schedule)
        return self

    def with_schedule(self, schedule: _ScheduleT, /) -> _ScheduleT:
        """Add a schedule to the component through a decorator call.

        Example
        -------
        This may be used in conjunction with `tanjun.as_interval`.

        ```py
        @component.with_schedule
        @tanjun.as_interval(60)
        async def my_schedule():
            print("I'm running every minute!")
        ```

        Parameters
        ----------
        schedule : schedules.AbstractSchedule
            The schedule to add.

        Returns
        -------
        schedules.AbstractSchedule
            The added schedule.
        """
        self.add_schedule(schedule)
        return schedule

    async def close(self, *, unbind: bool = False) -> None:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not self._loop:
            raise RuntimeError("Component isn't active")

        assert self._client

        for schedule in self._schedules:
            if schedule.is_alive:
                schedule.stop()

        self._loop = None
        # TODO: upgrade this to the standard interface
        assert isinstance(self._client, injecting.InjectorClient)
        await asyncio.gather(
            *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_close)
        )
        if unbind:
            self.unbind_client(self._client)

    async def open(self) -> None:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._loop:
            raise RuntimeError("Component is already active")

        if not self._client:
            raise RuntimeError("Client isn't bound yet")

        self._loop = asyncio.get_running_loop()
        # TODO: upgrade this to the standard interface
        assert isinstance(self._client, injecting.InjectorClient)
        await asyncio.gather(
            *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_open)
        )

        for schedule in self._schedules:
            schedule.start(self._client, loop=self._loop)

    def make_loader(self, *, copy: bool = True) -> tanjun_abc.ClientLoader:
        """Make a loader/unloader for this component.

        This enables loading, unloading and reloading of this component into a
        client by targeting the module using `tanjun.Client.load_modules`,
        `tanjun.Client.unload_modules` and `tanjun.Client.reload_modules`.

        Other Parameters
        ----------------
        copy: bool
            Whether to copy the component before loading it into a client.

            Defaults to `True`.

        Returns
        -------
        tanjun.abc.ClientLoader
            The loader for this component.
        """
        return _ComponentManager(self, copy)

Standard implementation of Tanjun's "components" used to manage separate features within a client.

#   class Component(tanjun.abc.Component):
View Source
class Component(tanjun_abc.Component):
    """Standard implementation of `tanjun.abc.Component`.

    This is a collcetion of commands (both message and slash), hooks and listener
    callbacks which can be added to a generic client.

    .. note::
        This implementation supports dependency injection for its checks,
        command callbacks and listeners when linked to a client which
        supports dependency injection.
    """

    __slots__ = (
        "_checks",
        "_client",
        "_client_callbacks",
        "_defaults_to_ephemeral",
        "_hooks",
        "_is_strict",
        "_listeners",
        "_loop",
        "_message_commands",
        "_message_hooks",
        "_metadata",
        "_name",
        "_names_to_commands",
        "_on_close",
        "_on_open",
        "_schedules",
        "_slash_commands",
        "_slash_hooks",
    )

    def __init__(self, *, name: typing.Optional[str] = None, strict: bool = False) -> None:
        """Initialise a new component.

        Other Parameters
        ----------------
        name : str
            The component's identifier.

            If not provided then this will be a random string.
        strict : bool
            Whether this component should use a stricter (more optimal) approach
            for message command search.

            When this is `True`, message command names will not be allowed to contain
            spaces and will have to be unique to one command within the component.
        """
        self._checks: list[checks_.InjectableCheck] = []
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._client_callbacks: dict[str, list[tanjun_abc.MetaEventSig]] = {}
        self._defaults_to_ephemeral: typing.Optional[bool] = None
        self._hooks: typing.Optional[tanjun_abc.AnyHooks] = None
        self._is_strict = strict
        self._listeners: dict[type[base_events.Event], list[tanjun_abc.ListenerCallbackSig]] = {}
        self._loop: typing.Optional[asyncio.AbstractEventLoop] = None
        self._message_commands: list[tanjun_abc.MessageCommand[typing.Any]] = []
        self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None
        self._metadata: dict[typing.Any, typing.Any] = {}
        self._name = name or base64.b64encode(random.randbytes(32)).decode()
        self._names_to_commands: dict[str, tanjun_abc.MessageCommand[typing.Any]] = {}
        self._on_close: list[injecting.CallbackDescriptor[None]] = []
        self._on_open: list[injecting.CallbackDescriptor[None]] = []
        self._schedules: list[schedules.AbstractSchedule] = []
        self._slash_commands: dict[str, tanjun_abc.BaseSlashCommand] = {}
        self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None

    def __repr__(self) -> str:
        return f"{type(self).__name__}({self.checks=}, {self.hooks=}, {self.slash_hooks=}, {self.message_hooks=})"

    @property
    def checks(self) -> collections.Collection[tanjun_abc.CheckSig]:
        """Collection of the checks being run against every command execution in this component."""
        return tuple(check.callback for check in self._checks)

    @property
    def client(self) -> typing.Optional[tanjun_abc.Client]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._client

    @property
    def defaults_to_ephemeral(self) -> typing.Optional[bool]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._defaults_to_ephemeral

    @property
    def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._hooks

    @property
    def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._loop

    @property
    def name(self) -> str:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._name

    @property
    def schedules(self) -> collections.Collection[schedules.AbstractSchedule]:
        """Collection of the schedules registered to this component."""
        return self._schedules

    @property
    def slash_commands(self) -> collections.Collection[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._slash_commands.copy().values()

    @property
    def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._slash_hooks

    @property
    def message_commands(self) -> collections.Collection[tanjun_abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._message_commands.copy()

    @property
    def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._message_hooks

    @property
    def needs_injector(self) -> bool:
        """Whether any of the checks in this component require dependency injection."""
        return any(check.needs_injector for check in self._checks)

    @property
    def listeners(
        self,
    ) -> collections.Mapping[type[base_events.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return utilities.CastedView(self._listeners, lambda x: x.copy())

    @property
    def metadata(self) -> dict[typing.Any, typing.Any]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return self._metadata

    def copy(self: _ComponentT, *, _new: bool = True) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not _new:
            self._checks = [check.copy() for check in self._checks]
            self._slash_commands = {name: command.copy() for name, command in self._slash_commands.items()}
            self._hooks = self._hooks.copy() if self._hooks else None
            self._listeners = {
                event: [copy.copy(listener) for listener in listeners] for event, listeners in self._listeners.items()
            }
            commands = {command: command.copy() for command in self._message_commands}
            self._message_commands = list(commands.values())
            self._metadata = self._metadata.copy()
            self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()}
            self._schedules = [schedule.copy() for schedule in self._schedules] if self._schedules else []
            return self

        return copy.copy(self).copy(_new=False)

    @typing.overload
    def load_from_scope(
        self: _ComponentT, *, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None
    ) -> _ComponentT:
        ...

    @typing.overload
    def load_from_scope(self: _ComponentT, *, include_globals: bool = False) -> _ComponentT:
        ...

    def load_from_scope(
        self: _ComponentT,
        *,
        include_globals: bool = False,
        scope: typing.Optional[collections.Mapping[str, typing.Any]] = None,
    ) -> _ComponentT:
        """Load entries such as top-level commands into the component from the calling scope.

        Notes
        -----
        * This will load schedules which support `AbstractComponentLoader`
          (e.g. `tanjun.schedules.IntervalSchedule`).
        * This will ignore commands which are owned by command groups.
        * This will detect entries from the calling scope which implement
          `AbstractComponentLoader` unless `scope` is passed but this isn't possible
          in a stack-less python implementation; in stack-less environments the
          scope will have to be explicitly passed as `scope`.

        Other Parameters
        ----------------
        include_globals: bool
            Whether to include global variables (along with local) while
            detecting from the calling scope.

            This defaults to `False`, cannot be `True` when `scope` is provided
            and will only ever be needed when the local scope is different
            from the global scope.
        scope : typing.Optional[collections.Mapping[str, typing.Any]]
            The scope to detect entries which implement `AbstractComponentLoader`
            from.

            This overrides the default usage of stackframe introspection.

        Returns
        -------
        Self
            The current component to allow for chaining.

        Raises
        ------
        RuntimeError
            If this is called in a python implementation which doesn't support
            stack frame inspection when `scope` is not provided.
        ValueError
            If `scope` is provided when `include_globals` is True.
        """
        if scope is None:
            if not (stack := inspect.currentframe()) or not stack.f_back:
                raise RuntimeError(
                    "Stackframe introspection is not supported in this runtime. Please explicitly pass `scope`."
                )

            values_iter = _filter_scope(stack.f_back.f_locals)
            if include_globals:
                values_iter = itertools.chain(values_iter, _filter_scope(stack.f_back.f_globals))

        elif include_globals:
            raise ValueError("Cannot specify include_globals as True when scope is passed")

        else:
            values_iter = _filter_scope(scope)

        _LOGGER.info(
            "Loading commands for %s component from %s parent scope(s)",
            self.name,
            "global and local" if include_globals else "local",
        )
        for value in values_iter:
            if isinstance(value, AbstractComponentLoader):
                value.load_into_component(self)

        return self

    def set_ephemeral_default(self: _ComponentT, state: typing.Optional[bool], /) -> _ComponentT:
        """Set whether slash contexts executed in this component should default to ephemeral responses.

        Parameters
        ----------
        typing.Optional[bool]
            Whether slash command contexts executed in this component should
            should default to ephemeral.
            This will be overridden by any response calls which specify flags.

            Setting this to `None` will let the default set on the parent
            client propagate and decide the ephemeral default behaviour.

        Returns
        -------
        SelfT
            This component to enable method chaining.
        """
        self._defaults_to_ephemeral = state
        return self

    def set_metadata(self: _ComponentT, key: typing.Any, value: typing.Any, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._metadata[key] = value
        return self

    def set_slash_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.SlashHooks], /) -> _ComponentT:
        self._slash_hooks = hooks_
        return self

    def set_message_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.MessageHooks], /) -> _ComponentT:
        self._message_hooks = hooks_
        return self

    def set_hooks(self: _ComponentT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ComponentT:
        self._hooks = hooks
        return self

    def add_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT:
        if check not in self._checks:
            self._checks.append(checks_.InjectableCheck(check))

        return self

    def remove_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT:
        self._checks.remove(typing.cast("checks_.InjectableCheck", check))
        return self

    def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT:
        self.add_check(check)
        return check

    def add_client_callback(
        self: _ComponentT,
        event_name: typing.Union[str, tanjun_abc.ClientCallbackNames],
        callback: tanjun_abc.MetaEventSig,
        /,
    ) -> _ComponentT:
        event_name = event_name.lower()
        try:
            if callback in self._client_callbacks[event_name]:
                return self

            self._client_callbacks[event_name].append(callback)
        except KeyError:
            self._client_callbacks[event_name] = [callback]

        if self._client:
            self._client.add_client_callback(event_name, callback)

        return self

    def get_client_callbacks(
        self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Collection[tanjun_abc.MetaEventSig]:
        event_name = event_name.lower()
        return self._client_callbacks.get(event_name) or ()

    def remove_client_callback(self, event_name: str, callback: tanjun_abc.MetaEventSig, /) -> None:
        event_name = event_name.lower()
        self._client_callbacks[event_name].remove(callback)
        if not self._client_callbacks[event_name]:
            del self._client_callbacks[event_name]

        if self._client:
            self._client.remove_client_callback(event_name, callback)

    def with_client_callback(
        self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]:
        def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT:
            self.add_client_callback(event_name, callback)
            return callback

        return decorator

    def add_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT:
        """Add a command to this component.

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to add.

        Returns
        -------
        Self
            The current component to allow for chaining.
        """
        if isinstance(command, tanjun_abc.MessageCommand):
            self.add_message_command(command)

        elif isinstance(command, tanjun_abc.BaseSlashCommand):
            self.add_slash_command(command)

        else:
            raise ValueError(
                f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}"
            )

        return self

    def remove_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT:
        """Remove a command from this component.

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to remove.

        Returns
        -------
        Self
            This component to enable method chaining.
        """
        if isinstance(command, tanjun_abc.MessageCommand):
            self.remove_message_command(command)

        elif isinstance(command, tanjun_abc.BaseSlashCommand):
            self.remove_slash_command(command)

        else:
            raise ValueError(
                f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}"
            )

        return self

    @typing.overload
    def with_command(self, command: CommandT, /) -> CommandT:
        ...

    @typing.overload
    def with_command(self, /, *, copy: bool = False) -> collections.Callable[[CommandT], CommandT]:
        ...

    def with_command(
        self, command: typing.Optional[CommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[CommandT]:
        """Add a command to this component through a decorator call.

        Examples
        --------
        This may be used inconjunction with `tanjun.as_slash_command`
        and `tanjun.as_message_command`.

        ```py
        @component.with_command
        @tanjun.with_slash_str_option("option_name", "option description")
        @tanjun.as_slash_command("command_name", "command description")
        async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None:
            await ctx.respond(f"Hi {arg}")
        ```

        ```py
        @component.with_command
        @tanjun.with_argument("argument_name")
        @tanjun.as_message_command("command_name")
        async def message_command(ctx: tanjun.abc.Context, arg: str) -> None:
            await ctx.respond(f"Hi {arg}")
        ```

        Parameters
        ----------
        command CommandT
            The command to add to this component.

        Other Parameters
        ----------------
        copy : bool
            Whether to copy the command before adding it to this component.

        Returns
        -------
        CommandT
            The added command.
        """
        return _with_command(self.add_command, command, copy=copy)

    def add_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._slash_commands.get(command.name) == command:
            return self

        command.bind_component(self)

        if self._client:
            command.bind_client(self._client)

        self._slash_commands[command.name.casefold()] = command
        return self

    def remove_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        try:
            del self._slash_commands[command.name.casefold()]
        except KeyError:
            raise ValueError(f"Command {command.name} not found") from None

        return self

    @typing.overload
    def with_slash_command(self, command: tanjun_abc.BaseSlashCommandT, /) -> tanjun_abc.BaseSlashCommandT:
        ...

    @typing.overload
    def with_slash_command(
        self, /, *, copy: bool = False
    ) -> collections.Callable[[tanjun_abc.BaseSlashCommandT], tanjun_abc.BaseSlashCommandT]:
        ...

    def with_slash_command(
        self, command: typing.Optional[tanjun_abc.BaseSlashCommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[tanjun_abc.BaseSlashCommandT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return _with_command(self.add_slash_command, command, copy=copy)

    def add_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT:
        """Add a message command to the component.

        Parameters
        ----------
        command : tanjun.abc.MessageCommand
            The command to add.

        Returns
        -------
        Self
            The component to allow method chaining.

        Raises
        ------
        ValueError
            If one of the command's name is already registered in a strict
            component.
        """
        if command in self._message_commands:
            return self

        if self._is_strict:
            if any(" " in name for name in command.names):
                raise ValueError("Command name cannot contain spaces for this component implementation")

            if name_conflicts := self._names_to_commands.keys() & command.names:
                raise ValueError(
                    "Sub-command names must be unique in a strict component. "
                    "The following conflicts were found " + ", ".join(name_conflicts)
                )

            self._names_to_commands.update((name, command) for name in command.names)

        self._message_commands.append(command)

        if self._client:
            command.bind_client(self._client)

        command.bind_component(self)
        return self

    def remove_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._message_commands.remove(command)

        if self._is_strict:
            for name in command.names:
                if self._names_to_commands.get(name) == command:
                    del self._names_to_commands[name]

        return self

    @typing.overload
    def with_message_command(self, command: tanjun_abc.MessageCommandT, /) -> tanjun_abc.MessageCommandT:
        ...

    @typing.overload
    def with_message_command(
        self, /, *, copy: bool = False
    ) -> collections.Callable[[tanjun_abc.MessageCommandT], tanjun_abc.MessageCommandT]:
        ...

    def with_message_command(
        self, command: typing.Optional[tanjun_abc.MessageCommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[tanjun_abc.MessageCommandT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return _with_command(self.add_message_command, command, copy=copy)

    def add_listener(
        self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, /
    ) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        try:
            if listener in self._listeners[event]:
                return self

            self._listeners[event].append(listener)

        except KeyError:
            self._listeners[event] = [listener]

        if self._client:
            self._client.add_listener(event, listener)

        return self

    def remove_listener(
        self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, /
    ) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._listeners[event].remove(listener)
        if not self._listeners[event]:
            del self._listeners[event]

        if self._client:
            self._client.remove_listener(event, listener)

        return self

    # TODO: make event optional?
    def with_listener(
        self, event_type: type[base_events.Event]
    ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        def decorator(callback: tanjun_abc.ListenerCallbackSigT) -> tanjun_abc.ListenerCallbackSigT:
            self.add_listener(event_type, callback)
            return callback

        return decorator

    def add_on_close(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT:
        """Add a close callback to this component.

        .. note::
            Unlike the closing and closed client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The close callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        Self
            The component object to enable call chaining.
        """
        self._on_close.append(injecting.CallbackDescriptor(callback))
        return self

    def with_on_close(self, callback: OnCallbackSigT, /) -> OnCallbackSigT:
        """Add a close callback to this component through a decorator call.

        .. note::
            Unlike the closing and closed client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The close callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        OnCallbackSig
            The added close callback.
        """
        self.add_on_close(callback)
        return callback

    def add_on_open(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT:
        """Add a open callback to this component.

        .. note::
            Unlike the starting and started client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The open callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        Self
            The component object to enable call chaining.
        """
        self._on_open.append(injecting.CallbackDescriptor(callback))
        return self

    def with_on_open(self, callback: OnCallbackSigT, /) -> OnCallbackSigT:
        """Add a open callback to this component through a decorator call.

        .. note::
            Unlike the starting and started client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The open callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        OnCallbackSig
            The added open callback.
        """
        self.add_on_open(callback)
        return callback

    def bind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._client:
            raise RuntimeError("Client already set")

        self._client = client
        for message_command in self._message_commands:
            message_command.bind_client(client)

        for slash_command in self._slash_commands.values():
            slash_command.bind_client(client)

        for event, listeners in self._listeners.items():
            for listener in listeners:
                self._client.add_listener(event, listener)

        for event_name, callbacks in self._client_callbacks.items():
            for callback in callbacks:
                self._client.add_client_callback(event_name, callback)

        return self

    def unbind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not self._client or self._client != client:
            raise RuntimeError("Component isn't bound to this client")

        for event, listeners in self._listeners.items():
            for listener in listeners:
                try:
                    self._client.remove_listener(event, listener)
                except (LookupError, ValueError):
                    pass

        for event_name, callbacks in self._client_callbacks.items():
            for callback in callbacks:
                try:
                    self._client.remove_client_callback(event_name, callback)
                except (LookupError, ValueError):
                    pass

        self._client = None

        return self

    async def _check_context(self, ctx: tanjun_abc.Context, /) -> bool:
        return await utilities.gather_checks(ctx, self._checks)

    async def _check_message_context(
        self, ctx: tanjun_abc.MessageContext, /
    ) -> collections.AsyncIterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        ctx.set_component(self)

        if self._is_strict:
            name = ctx.content.split(" ", 1)[0]
            command = self._names_to_commands.get(name)
            if command and await self._check_context(ctx) and await command.check_context(ctx):
                yield name, command

            else:
                ctx.set_component(None)

            return

        checks_run = False
        for name, command in self.check_message_name(ctx.content):
            if not checks_run:
                if not await self._check_context(ctx):
                    return

                checks_run = True

            if await command.check_context(ctx):
                yield name, command

        ctx.set_component(None)

    def check_message_name(
        self, content: str, /
    ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._is_strict:
            name = content.split(" ", 1)[0]
            if command := self._names_to_commands.get(name):
                yield name, command
            return

        for command in self._message_commands:
            if (name_ := utilities.match_prefix_names(content, command.names)) is not None:
                yield name_, command

    def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Component>>.
        if command := self._slash_commands.get(name):
            yield command

    async def _execute_interaction(
        self,
        ctx: tanjun_abc.SlashContext,
        command: typing.Optional[tanjun_abc.BaseSlashCommand],
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None,
    ) -> typing.Optional[collections.Awaitable[None]]:
        try:
            if not command or not await self._check_context(ctx) or not await command.check_context(ctx):
                return None

        except errors.HaltExecution:
            return asyncio.get_running_loop().create_task(ctx.mark_not_found())

        except errors.CommandError as exc:
            await ctx.respond(exc.message)
            asyncio.get_running_loop().create_future().set_result(None)
            return None

        if self._slash_hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._slash_hooks)

        if self._hooks:
            if hooks is None:
                hooks = set()

            hooks.add(self._hooks)

        return asyncio.get_running_loop().create_task(command.execute(ctx, hooks=hooks))

    # To ensure that ctx.set_ephemeral_default is called as soon as possible if
    # a match is found the public function is kept sync to avoid yielding
    # to the event loop until after this is set.
    def execute_interaction(
        self,
        ctx: tanjun_abc.SlashContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None,
    ) -> collections.Coroutine[typing.Any, typing.Any, typing.Optional[collections.Awaitable[None]]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        command = self._slash_commands.get(ctx.interaction.command_name)
        if command:
            if command.defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(command.defaults_to_ephemeral)

            elif self._defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(self._defaults_to_ephemeral)

        return self._execute_interaction(ctx, command, hooks=hooks)

    async def execute_message(
        self,
        ctx: tanjun_abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.MessageHooks]] = None,
    ) -> bool:
        # <<inherited docstring from tanjun.abc.Component>>.
        async for name, command in self._check_message_context(ctx):
            ctx.set_triggering_name(name)
            ctx.set_content(ctx.content[len(name) :].lstrip())
            ctx.set_component(self)
            # Only add our hooks if we're sure we'll be executing the command here.

            if self._message_hooks:
                if hooks is None:
                    hooks = set()

                hooks.add(self._message_hooks)

            if self._hooks:
                if hooks is None:
                    hooks = set()

                hooks.add(self._hooks)

            await command.execute(ctx, hooks=hooks)
            return True

        ctx.set_component(None)
        return False

    def _load_from_properties(self) -> None:
        for _, member in inspect.getmembers(self):
            if isinstance(member, AbstractComponentLoader):
                member.load_into_component(self)

    def add_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT:
        """Add a schedule to the component.

        Parameters
        ----------
        schedule : tanjun.schedules.AbstractSchedule
            The schedule to add.

        Returns
        -------
        Self
            The component itself for chaining.
        """
        if self._client and self._loop:
            # TODO: upgrade this to the standard interface
            assert isinstance(self._client, injecting.InjectorClient)
            schedule.start(self._client, loop=self._loop)

        self._schedules.append(schedule)
        return self

    def remove_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT:
        """Remove a schedule from the component.

        Parameters
        ----------
        schedule : tanjun.schedules.AbstractSchedule
            The schedule to remove

        Returns
        -------
        Self
            The component itself for chaining.

        Raises
        ------
        ValueError
            If the schedule isn't registered.
        """
        if schedule.is_alive:
            schedule.stop()

        self._schedules.remove(schedule)
        return self

    def with_schedule(self, schedule: _ScheduleT, /) -> _ScheduleT:
        """Add a schedule to the component through a decorator call.

        Example
        -------
        This may be used in conjunction with `tanjun.as_interval`.

        ```py
        @component.with_schedule
        @tanjun.as_interval(60)
        async def my_schedule():
            print("I'm running every minute!")
        ```

        Parameters
        ----------
        schedule : schedules.AbstractSchedule
            The schedule to add.

        Returns
        -------
        schedules.AbstractSchedule
            The added schedule.
        """
        self.add_schedule(schedule)
        return schedule

    async def close(self, *, unbind: bool = False) -> None:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not self._loop:
            raise RuntimeError("Component isn't active")

        assert self._client

        for schedule in self._schedules:
            if schedule.is_alive:
                schedule.stop()

        self._loop = None
        # TODO: upgrade this to the standard interface
        assert isinstance(self._client, injecting.InjectorClient)
        await asyncio.gather(
            *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_close)
        )
        if unbind:
            self.unbind_client(self._client)

    async def open(self) -> None:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._loop:
            raise RuntimeError("Component is already active")

        if not self._client:
            raise RuntimeError("Client isn't bound yet")

        self._loop = asyncio.get_running_loop()
        # TODO: upgrade this to the standard interface
        assert isinstance(self._client, injecting.InjectorClient)
        await asyncio.gather(
            *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_open)
        )

        for schedule in self._schedules:
            schedule.start(self._client, loop=self._loop)

    def make_loader(self, *, copy: bool = True) -> tanjun_abc.ClientLoader:
        """Make a loader/unloader for this component.

        This enables loading, unloading and reloading of this component into a
        client by targeting the module using `tanjun.Client.load_modules`,
        `tanjun.Client.unload_modules` and `tanjun.Client.reload_modules`.

        Other Parameters
        ----------------
        copy: bool
            Whether to copy the component before loading it into a client.

            Defaults to `True`.

        Returns
        -------
        tanjun.abc.ClientLoader
            The loader for this component.
        """
        return _ComponentManager(self, copy)

Standard implementation of tanjun.abc.Component.

This is a collcetion of commands (both message and slash), hooks and listener callbacks which can be added to a generic client.

Note: This implementation supports dependency injection for its checks, command callbacks and listeners when linked to a client which supports dependency injection.

#   Component(*, name: Optional[str] = None, strict: bool = False)
View Source
    def __init__(self, *, name: typing.Optional[str] = None, strict: bool = False) -> None:
        """Initialise a new component.

        Other Parameters
        ----------------
        name : str
            The component's identifier.

            If not provided then this will be a random string.
        strict : bool
            Whether this component should use a stricter (more optimal) approach
            for message command search.

            When this is `True`, message command names will not be allowed to contain
            spaces and will have to be unique to one command within the component.
        """
        self._checks: list[checks_.InjectableCheck] = []
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._client_callbacks: dict[str, list[tanjun_abc.MetaEventSig]] = {}
        self._defaults_to_ephemeral: typing.Optional[bool] = None
        self._hooks: typing.Optional[tanjun_abc.AnyHooks] = None
        self._is_strict = strict
        self._listeners: dict[type[base_events.Event], list[tanjun_abc.ListenerCallbackSig]] = {}
        self._loop: typing.Optional[asyncio.AbstractEventLoop] = None
        self._message_commands: list[tanjun_abc.MessageCommand[typing.Any]] = []
        self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None
        self._metadata: dict[typing.Any, typing.Any] = {}
        self._name = name or base64.b64encode(random.randbytes(32)).decode()
        self._names_to_commands: dict[str, tanjun_abc.MessageCommand[typing.Any]] = {}
        self._on_close: list[injecting.CallbackDescriptor[None]] = []
        self._on_open: list[injecting.CallbackDescriptor[None]] = []
        self._schedules: list[schedules.AbstractSchedule] = []
        self._slash_commands: dict[str, tanjun_abc.BaseSlashCommand] = {}
        self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None

Initialise a new component.

Other Parameters
  • name (str): The component's identifier.

    If not provided then this will be a random string.

  • strict (bool): Whether this component should use a stricter (more optimal) approach for message command search.

    When this is True, message command names will not be allowed to contain spaces and will have to be unique to one command within the component.

#   checks: collections.abc.Collection[collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]]]

Collection of the checks being run against every command execution in this component.

#   client: Optional[tanjun.abc.Client]

Tanjun client this component is bound to.

#   defaults_to_ephemeral: Optional[bool]

Whether slash contexts executed in this component should default to ephemeral responses.

This effects calls to SlashContext.create_followup, SlashContext.create_initial_response, SlashContext.defer and SlashContext.respond unless the flags field is provided for the methods which support it.

Notes
  • This may be overridden by BaseSlashCommand.defaults_to_ephemeral.
  • This only effects slash command execution.
  • If this is None then the default from the parent client is used.
#   loop: Optional[asyncio.events.AbstractEventLoop]

The asyncio loop this client is bound to if it has been opened.

#   name: str

Component's unique identifier.

Note: This will be preserved between copies of a component.

#   schedules: collections.abc.Collection[tanjun.schedules.AbstractSchedule]

Collection of the schedules registered to this component.

#   slash_commands: collections.abc.Collection[tanjun.abc.BaseSlashCommand]

Collection of the slash commands in this component.

#   message_commands: collections.abc.Collection[tanjun.abc.MessageCommand[typing.Any]]

Collection of the message commands in this component.

#   needs_injector: bool

Whether any of the checks in this component require dependency injection.

#   listeners: collections.abc.Mapping[type[hikari.events.base_events.Event], collections.abc.Collection[collections.abc.Callable[..., collections.abc.Coroutine[typing.Any, typing.Any, None]]]]

Mapping of event types to the listeners registered for them in this component.

#   metadata: dict[typing.Any, typing.Any]

Mutable mapping of the metadata set for this component.

Note: Any modifications made to this mutable mapping will be preserved by the component.

#   def copy(self: ~_ComponentT, *, _new: bool = True) -> ~_ComponentT:
View Source
    def copy(self: _ComponentT, *, _new: bool = True) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not _new:
            self._checks = [check.copy() for check in self._checks]
            self._slash_commands = {name: command.copy() for name, command in self._slash_commands.items()}
            self._hooks = self._hooks.copy() if self._hooks else None
            self._listeners = {
                event: [copy.copy(listener) for listener in listeners] for event, listeners in self._listeners.items()
            }
            commands = {command: command.copy() for command in self._message_commands}
            self._message_commands = list(commands.values())
            self._metadata = self._metadata.copy()
            self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()}
            self._schedules = [schedule.copy() for schedule in self._schedules] if self._schedules else []
            return self

        return copy.copy(self).copy(_new=False)
#   def load_from_scope( self: ~_ComponentT, *, include_globals: bool = False, scope: Optional[collections.abc.Mapping[str, Any]] = None ) -> ~_ComponentT:
View Source
    def load_from_scope(
        self: _ComponentT,
        *,
        include_globals: bool = False,
        scope: typing.Optional[collections.Mapping[str, typing.Any]] = None,
    ) -> _ComponentT:
        """Load entries such as top-level commands into the component from the calling scope.

        Notes
        -----
        * This will load schedules which support `AbstractComponentLoader`
          (e.g. `tanjun.schedules.IntervalSchedule`).
        * This will ignore commands which are owned by command groups.
        * This will detect entries from the calling scope which implement
          `AbstractComponentLoader` unless `scope` is passed but this isn't possible
          in a stack-less python implementation; in stack-less environments the
          scope will have to be explicitly passed as `scope`.

        Other Parameters
        ----------------
        include_globals: bool
            Whether to include global variables (along with local) while
            detecting from the calling scope.

            This defaults to `False`, cannot be `True` when `scope` is provided
            and will only ever be needed when the local scope is different
            from the global scope.
        scope : typing.Optional[collections.Mapping[str, typing.Any]]
            The scope to detect entries which implement `AbstractComponentLoader`
            from.

            This overrides the default usage of stackframe introspection.

        Returns
        -------
        Self
            The current component to allow for chaining.

        Raises
        ------
        RuntimeError
            If this is called in a python implementation which doesn't support
            stack frame inspection when `scope` is not provided.
        ValueError
            If `scope` is provided when `include_globals` is True.
        """
        if scope is None:
            if not (stack := inspect.currentframe()) or not stack.f_back:
                raise RuntimeError(
                    "Stackframe introspection is not supported in this runtime. Please explicitly pass `scope`."
                )

            values_iter = _filter_scope(stack.f_back.f_locals)
            if include_globals:
                values_iter = itertools.chain(values_iter, _filter_scope(stack.f_back.f_globals))

        elif include_globals:
            raise ValueError("Cannot specify include_globals as True when scope is passed")

        else:
            values_iter = _filter_scope(scope)

        _LOGGER.info(
            "Loading commands for %s component from %s parent scope(s)",
            self.name,
            "global and local" if include_globals else "local",
        )
        for value in values_iter:
            if isinstance(value, AbstractComponentLoader):
                value.load_into_component(self)

        return self

Load entries such as top-level commands into the component from the calling scope.

Notes
  • This will load schedules which support AbstractComponentLoader (e.g. tanjun.schedules.IntervalSchedule).
  • This will ignore commands which are owned by command groups.
  • This will detect entries from the calling scope which implement AbstractComponentLoader unless scope is passed but this isn't possible in a stack-less python implementation; in stack-less environments the scope will have to be explicitly passed as scope.
Other Parameters
  • include_globals (bool): Whether to include global variables (along with local) while detecting from the calling scope.

    This defaults to False, cannot be True when scope is provided and will only ever be needed when the local scope is different from the global scope.

  • scope (typing.Optional[collections.Mapping[str, typing.Any]]): The scope to detect entries which implement AbstractComponentLoader from.

    This overrides the default usage of stackframe introspection.

Returns
  • Self: The current component to allow for chaining.
Raises
  • RuntimeError: If this is called in a python implementation which doesn't support stack frame inspection when scope is not provided.
  • ValueError: If scope is provided when include_globals is True.
#   def set_ephemeral_default(self: ~_ComponentT, state: Optional[bool], /) -> ~_ComponentT:
View Source
    def set_ephemeral_default(self: _ComponentT, state: typing.Optional[bool], /) -> _ComponentT:
        """Set whether slash contexts executed in this component should default to ephemeral responses.

        Parameters
        ----------
        typing.Optional[bool]
            Whether slash command contexts executed in this component should
            should default to ephemeral.
            This will be overridden by any response calls which specify flags.

            Setting this to `None` will let the default set on the parent
            client propagate and decide the ephemeral default behaviour.

        Returns
        -------
        SelfT
            This component to enable method chaining.
        """
        self._defaults_to_ephemeral = state
        return self

Set whether slash contexts executed in this component should default to ephemeral responses.

Parameters
  • typing.Optional[bool]: Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags.

Setting this to None will let the default set on the parent client propagate and decide the ephemeral default behaviour.

Returns
  • SelfT: This component to enable method chaining.
#   def set_metadata(self: ~_ComponentT, key: Any, value: Any, /) -> ~_ComponentT:
View Source
    def set_metadata(self: _ComponentT, key: typing.Any, value: typing.Any, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._metadata[key] = value
        return self

Set a field in the component's metadata.

Parameters
  • key (typing.Any): Metadata key to set.
  • value (typing.Any): Metadata value to set.
Returns
  • Self: The component instance to enable chained calls.
#   def set_slash_hooks( self: ~_ComponentT, hooks_: Optional[tanjun.abc.Hooks[tanjun.abc.SlashContext]], / ) -> ~_ComponentT:
View Source
    def set_slash_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.SlashHooks], /) -> _ComponentT:
        self._slash_hooks = hooks_
        return self
#   def set_message_hooks( self: ~_ComponentT, hooks_: Optional[tanjun.abc.Hooks[tanjun.abc.MessageContext]], / ) -> ~_ComponentT:
View Source
    def set_message_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.MessageHooks], /) -> _ComponentT:
        self._message_hooks = hooks_
        return self
#   def set_hooks( self: ~_ComponentT, hooks: Optional[tanjun.abc.Hooks[tanjun.abc.Context]], / ) -> ~_ComponentT:
View Source
    def set_hooks(self: _ComponentT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ComponentT:
        self._hooks = hooks
        return self
#   def add_check( self: ~_ComponentT, check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], / ) -> ~_ComponentT:
View Source
    def add_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT:
        if check not in self._checks:
            self._checks.append(checks_.InjectableCheck(check))

        return self
#   def remove_check( self: ~_ComponentT, check: collections.abc.Callable[..., typing.Union[bool, collections.abc.Awaitable[bool]]], / ) -> ~_ComponentT:
View Source
    def remove_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT:
        self._checks.remove(typing.cast("checks_.InjectableCheck", check))
        return self
#   def with_check(self, check: ~CheckSigT, /) -> ~CheckSigT:
View Source
    def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT:
        self.add_check(check)
        return check
#   def add_client_callback( self: ~_ComponentT, event_name: Union[str, tanjun.abc.ClientCallbackNames], callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_ComponentT:
View Source
    def add_client_callback(
        self: _ComponentT,
        event_name: typing.Union[str, tanjun_abc.ClientCallbackNames],
        callback: tanjun_abc.MetaEventSig,
        /,
    ) -> _ComponentT:
        event_name = event_name.lower()
        try:
            if callback in self._client_callbacks[event_name]:
                return self

            self._client_callbacks[event_name].append(callback)
        except KeyError:
            self._client_callbacks[event_name] = [callback]

        if self._client:
            self._client.add_client_callback(event_name, callback)

        return self
#   def get_client_callbacks( self, event_name: Union[str, tanjun.abc.ClientCallbackNames], / ) -> collections.abc.Collection[collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]]]:
View Source
    def get_client_callbacks(
        self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Collection[tanjun_abc.MetaEventSig]:
        event_name = event_name.lower()
        return self._client_callbacks.get(event_name) or ()
#   def remove_client_callback( self, event_name: str, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> None:
View Source
    def remove_client_callback(self, event_name: str, callback: tanjun_abc.MetaEventSig, /) -> None:
        event_name = event_name.lower()
        self._client_callbacks[event_name].remove(callback)
        if not self._client_callbacks[event_name]:
            del self._client_callbacks[event_name]

        if self._client:
            self._client.remove_client_callback(event_name, callback)
#   def with_client_callback( self, event_name: Union[str, tanjun.abc.ClientCallbackNames], / ) -> collections.abc.Callable[[~MetaEventSigT], ~MetaEventSigT]:
View Source
    def with_client_callback(
        self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], /
    ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]:
        def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT:
            self.add_client_callback(event_name, callback)
            return callback

        return decorator
#   def add_command( self: ~_ComponentT, command: tanjun.abc.ExecutableCommand[typing.Any], / ) -> ~_ComponentT:
View Source
    def add_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT:
        """Add a command to this component.

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to add.

        Returns
        -------
        Self
            The current component to allow for chaining.
        """
        if isinstance(command, tanjun_abc.MessageCommand):
            self.add_message_command(command)

        elif isinstance(command, tanjun_abc.BaseSlashCommand):
            self.add_slash_command(command)

        else:
            raise ValueError(
                f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}"
            )

        return self

Add a command to this component.

Parameters
Returns
  • Self: The current component to allow for chaining.
#   def remove_command( self: ~_ComponentT, command: tanjun.abc.ExecutableCommand[typing.Any], / ) -> ~_ComponentT:
View Source
    def remove_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT:
        """Remove a command from this component.

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to remove.

        Returns
        -------
        Self
            This component to enable method chaining.
        """
        if isinstance(command, tanjun_abc.MessageCommand):
            self.remove_message_command(command)

        elif isinstance(command, tanjun_abc.BaseSlashCommand):
            self.remove_slash_command(command)

        else:
            raise ValueError(
                f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}"
            )

        return self

Remove a command from this component.

Parameters
Returns
  • Self: This component to enable method chaining.
#   def with_command( self, command: Optional[~CommandT] = None, /, *, copy: bool = False ) -> Union[~CommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
    def with_command(
        self, command: typing.Optional[CommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[CommandT]:
        """Add a command to this component through a decorator call.

        Examples
        --------
        This may be used inconjunction with `tanjun.as_slash_command`
        and `tanjun.as_message_command`.

        ```py
        @component.with_command
        @tanjun.with_slash_str_option("option_name", "option description")
        @tanjun.as_slash_command("command_name", "command description")
        async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None:
            await ctx.respond(f"Hi {arg}")
        ```

        ```py
        @component.with_command
        @tanjun.with_argument("argument_name")
        @tanjun.as_message_command("command_name")
        async def message_command(ctx: tanjun.abc.Context, arg: str) -> None:
            await ctx.respond(f"Hi {arg}")
        ```

        Parameters
        ----------
        command CommandT
            The command to add to this component.

        Other Parameters
        ----------------
        copy : bool
            Whether to copy the command before adding it to this component.

        Returns
        -------
        CommandT
            The added command.
        """
        return _with_command(self.add_command, command, copy=copy)

Add a command to this component through a decorator call.

Examples

This may be used inconjunction with tanjun.as_slash_command and tanjun.as_message_command.

@component.with_command
@tanjun.with_slash_str_option("option_name", "option description")
@tanjun.as_slash_command("command_name", "command description")
async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None:
    await ctx.respond(f"Hi {arg}")
@component.with_command
@tanjun.with_argument("argument_name")
@tanjun.as_message_command("command_name")
async def message_command(ctx: tanjun.abc.Context, arg: str) -> None:
    await ctx.respond(f"Hi {arg}")
Parameters
  • command CommandT: The command to add to this component.
Other Parameters
  • copy (bool): Whether to copy the command before adding it to this component.
Returns
  • CommandT: The added command.
#   def add_slash_command( self: ~_ComponentT, command: tanjun.abc.BaseSlashCommand, / ) -> ~_ComponentT:
View Source
    def add_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._slash_commands.get(command.name) == command:
            return self

        command.bind_component(self)

        if self._client:
            command.bind_client(self._client)

        self._slash_commands[command.name.casefold()] = command
        return self

Add a slash command to this component.

Parameters
  • command (BaseSlashCommand): The command to add.
Returns
  • Self: The component to enable chained calls.
#   def remove_slash_command( self: ~_ComponentT, command: tanjun.abc.BaseSlashCommand, / ) -> ~_ComponentT:
View Source
    def remove_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        try:
            del self._slash_commands[command.name.casefold()]
        except KeyError:
            raise ValueError(f"Command {command.name} not found") from None

        return self

Remove a slash command from this component.

Parameters
  • command (BaseSlashCommand): The command to remove.
Raises
  • ValueError: If the provided command isn't found.
Returns
  • Self: The component to enable chained calls.
#   def with_slash_command( self, command: Optional[~BaseSlashCommandT] = None, /, *, copy: bool = False ) -> Union[~BaseSlashCommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
    def with_slash_command(
        self, command: typing.Optional[tanjun_abc.BaseSlashCommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[tanjun_abc.BaseSlashCommandT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return _with_command(self.add_slash_command, command, copy=copy)

Add a slash command to this component through a decorator call.

Parameters
  • command (BaseSlashCommandT): The command to add.
Other Parameters
  • copy (bool): Whether to copy the command before adding it.
Returns
  • BaseSlashCommandT: The added command.
#   def add_message_command( self: ~_ComponentT, command: tanjun.abc.MessageCommand[typing.Any], / ) -> ~_ComponentT:
View Source
    def add_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT:
        """Add a message command to the component.

        Parameters
        ----------
        command : tanjun.abc.MessageCommand
            The command to add.

        Returns
        -------
        Self
            The component to allow method chaining.

        Raises
        ------
        ValueError
            If one of the command's name is already registered in a strict
            component.
        """
        if command in self._message_commands:
            return self

        if self._is_strict:
            if any(" " in name for name in command.names):
                raise ValueError("Command name cannot contain spaces for this component implementation")

            if name_conflicts := self._names_to_commands.keys() & command.names:
                raise ValueError(
                    "Sub-command names must be unique in a strict component. "
                    "The following conflicts were found " + ", ".join(name_conflicts)
                )

            self._names_to_commands.update((name, command) for name in command.names)

        self._message_commands.append(command)

        if self._client:
            command.bind_client(self._client)

        command.bind_component(self)
        return self

Add a message command to the component.

Parameters
Returns
  • Self: The component to allow method chaining.
Raises
  • ValueError: If one of the command's name is already registered in a strict component.
#   def remove_message_command( self: ~_ComponentT, command: tanjun.abc.MessageCommand[typing.Any], / ) -> ~_ComponentT:
View Source
    def remove_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._message_commands.remove(command)

        if self._is_strict:
            for name in command.names:
                if self._names_to_commands.get(name) == command:
                    del self._names_to_commands[name]

        return self

Remove a message command from this component.

Parameters
  • command (MessageCommand[typing.Any]): The command to remove.
Raises
  • ValueError: If the provided command isn't found.
Returns
  • Self: The component to enable chained calls.
#   def with_message_command( self, command: Optional[~MessageCommandT] = None, /, *, copy: bool = False ) -> Union[~MessageCommandT, collections.abc.Callable[[~CommandT], ~CommandT]]:
View Source
    def with_message_command(
        self, command: typing.Optional[tanjun_abc.MessageCommandT] = None, /, *, copy: bool = False
    ) -> WithCommandReturnSig[tanjun_abc.MessageCommandT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        return _with_command(self.add_message_command, command, copy=copy)

Add a message command to this component through a decorator call.

Parameters
  • command (MessageCommandT): The command to add.
Other Parameters
  • copy (bool): Whether to copy the command before adding it.
Returns
  • MessageCommandT: The added command.
#   def add_listener( self: ~_ComponentT, event: type[hikari.events.base_events.Event], listener: collections.abc.Callable[..., collections.abc.Coroutine[typing.Any, typing.Any, None]], / ) -> ~_ComponentT:
View Source
    def add_listener(
        self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, /
    ) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        try:
            if listener in self._listeners[event]:
                return self

            self._listeners[event].append(listener)

        except KeyError:
            self._listeners[event] = [listener]

        if self._client:
            self._client.add_listener(event, listener)

        return self

Add a listener to this component.

Parameters
  • event (type[hikari.Event]): The event to listen for.
  • listener (ListenerCallbackSig): The listener to add.
Returns
  • Self: The component to enable chained calls.
#   def remove_listener( self: ~_ComponentT, event: type[hikari.events.base_events.Event], listener: collections.abc.Callable[..., collections.abc.Coroutine[typing.Any, typing.Any, None]], / ) -> ~_ComponentT:
View Source
    def remove_listener(
        self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, /
    ) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        self._listeners[event].remove(listener)
        if not self._listeners[event]:
            del self._listeners[event]

        if self._client:
            self._client.remove_listener(event, listener)

        return self

Remove a listener from this component.

Parameters
  • event (type[hikari.Event]): The event to listen for.
  • listener (ListenerCallbackSig): The listener to remove.
Raises
  • ValueError: If the listener is not registered for the provided event.
Returns
  • Self: The component to enable chained calls.
#   def with_listener( self, event_type: type[hikari.events.base_events.Event] ) -> collections.abc.Callable[[~ListenerCallbackSigT], ~ListenerCallbackSigT]:
View Source
    def with_listener(
        self, event_type: type[base_events.Event]
    ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]:
        # <<inherited docstring from tanjun.abc.Component>>.
        def decorator(callback: tanjun_abc.ListenerCallbackSigT) -> tanjun_abc.ListenerCallbackSigT:
            self.add_listener(event_type, callback)
            return callback

        return decorator

Add a listener to this component through a decorator call.

Parameters
  • event_type (type[hikari.Event]): The event to listen for.
Returns
  • collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]: Decorator callback which takes listener to add.
#   def add_on_close( self: ~_ComponentT, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_ComponentT:
View Source
    def add_on_close(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT:
        """Add a close callback to this component.

        .. note::
            Unlike the closing and closed client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The close callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        Self
            The component object to enable call chaining.
        """
        self._on_close.append(injecting.CallbackDescriptor(callback))
        return self

Add a close callback to this component.

Note: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.

Parameters
  • callback (OnCallbackSig): The close callback to add to this component.

    This should take no positional arguments, return None and may take use injected dependencies.

Returns
  • Self: The component object to enable call chaining.
#   def with_on_close(self, callback: ~OnCallbackSigT, /) -> ~OnCallbackSigT:
View Source
    def with_on_close(self, callback: OnCallbackSigT, /) -> OnCallbackSigT:
        """Add a close callback to this component through a decorator call.

        .. note::
            Unlike the closing and closed client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The close callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        OnCallbackSig
            The added close callback.
        """
        self.add_on_close(callback)
        return callback

Add a close callback to this component through a decorator call.

Note: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.

Parameters
  • callback (OnCallbackSig): The close callback to add to this component.

    This should take no positional arguments, return None and may take use injected dependencies.

Returns
  • OnCallbackSig: The added close callback.
#   def add_on_open( self: ~_ComponentT, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_ComponentT:
View Source
    def add_on_open(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT:
        """Add a open callback to this component.

        .. note::
            Unlike the starting and started client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The open callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        Self
            The component object to enable call chaining.
        """
        self._on_open.append(injecting.CallbackDescriptor(callback))
        return self

Add a open callback to this component.

Note: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.

Parameters
  • callback (OnCallbackSig): The open callback to add to this component.

    This should take no positional arguments, return None and may take use injected dependencies.

Returns
  • Self: The component object to enable call chaining.
#   def with_on_open(self, callback: ~OnCallbackSigT, /) -> ~OnCallbackSigT:
View Source
    def with_on_open(self, callback: OnCallbackSigT, /) -> OnCallbackSigT:
        """Add a open callback to this component through a decorator call.

        .. note::
            Unlike the starting and started client callbacks, this is only
            called for the current component's lifetime and is guaranteed to be
            called regardless of when the component was added to a client.

        Parameters
        ----------
        callback : OnCallbackSig
            The open callback to add to this component.

            This should take no positional arguments, return `None` and may
            take use injected dependencies.

        Returns
        -------
        OnCallbackSig
            The added open callback.
        """
        self.add_on_open(callback)
        return callback

Add a open callback to this component through a decorator call.

Note: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.

Parameters
  • callback (OnCallbackSig): The open callback to add to this component.

    This should take no positional arguments, return None and may take use injected dependencies.

Returns
  • OnCallbackSig: The added open callback.
#   def bind_client(self: ~_ComponentT, client: tanjun.abc.Client, /) -> ~_ComponentT:
View Source
    def bind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._client:
            raise RuntimeError("Client already set")

        self._client = client
        for message_command in self._message_commands:
            message_command.bind_client(client)

        for slash_command in self._slash_commands.values():
            slash_command.bind_client(client)

        for event, listeners in self._listeners.items():
            for listener in listeners:
                self._client.add_listener(event, listener)

        for event_name, callbacks in self._client_callbacks.items():
            for callback in callbacks:
                self._client.add_client_callback(event_name, callback)

        return self
#   def unbind_client(self: ~_ComponentT, client: tanjun.abc.Client, /) -> ~_ComponentT:
View Source
    def unbind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not self._client or self._client != client:
            raise RuntimeError("Component isn't bound to this client")

        for event, listeners in self._listeners.items():
            for listener in listeners:
                try:
                    self._client.remove_listener(event, listener)
                except (LookupError, ValueError):
                    pass

        for event_name, callbacks in self._client_callbacks.items():
            for callback in callbacks:
                try:
                    self._client.remove_client_callback(event_name, callback)
                except (LookupError, ValueError):
                    pass

        self._client = None

        return self
#   def check_message_name( self, content: str, / ) -> collections.abc.Iterator[tuple[str, tanjun.abc.MessageCommand[typing.Any]]]:
View Source
    def check_message_name(
        self, content: str, /
    ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._is_strict:
            name = content.split(" ", 1)[0]
            if command := self._names_to_commands.get(name):
                yield name, command
            return

        for command in self._message_commands:
            if (name_ := utilities.match_prefix_names(content, command.names)) is not None:
                yield name_, command

Check whether a name matches any of this component's registered message commands.

Notes
  • This only checks for name matches against the top level command and will not account for sub-commands.
  • Dependent on implementation detail this may partial check name against command names using name.startswith(command_name), hence why it also returns the name a command was matched by.
Parameters
  • name (str): The name to check for command matches.
Returns
  • collections.abc.Iterator[tuple[str, MessageCommand[typing.Any]]]: Iterator of tuples of command name matches to the relevant message command objects.
#   def check_slash_name( self, name: str, / ) -> collections.abc.Iterator[tanjun.abc.BaseSlashCommand]:
View Source
    def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.Component>>.
        if command := self._slash_commands.get(name):
            yield command

Check whether a name matches any of this component's registered slash commands.

Note: This won't check for sub-commands and will expect name to simply be the top level command name.

Parameters
  • name (str): The name to check for command matches.
Returns
  • collections.abc.Iterator[BaseSlashCommand]: An iterator of the matching slash commands.
#   def execute_interaction( self, ctx: tanjun.abc.SlashContext, /, *, hooks: Optional[collections.abc.MutableSet[tanjun.abc.Hooks[tanjun.abc.SlashContext]]] = None ) -> collections.abc.Coroutine[typing.Any, typing.Any, typing.Optional[collections.abc.Awaitable[None]]]:
View Source
    def execute_interaction(
        self,
        ctx: tanjun_abc.SlashContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None,
    ) -> collections.Coroutine[typing.Any, typing.Any, typing.Optional[collections.Awaitable[None]]]:
        # <<inherited docstring from tanjun.abc.Component>>.
        command = self._slash_commands.get(ctx.interaction.command_name)
        if command:
            if command.defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(command.defaults_to_ephemeral)

            elif self._defaults_to_ephemeral is not None:
                ctx.set_ephemeral_default(self._defaults_to_ephemeral)

        return self._execute_interaction(ctx, command, hooks=hooks)

Execute a slash context.

Note: Unlike Component.execute_message, this shouldn't be expected to raise tanjun.errors.HaltExecution nor tanjun.errors.CommandError.

Parameters
  • ctx (SlashContext): The context to execute.
Other Parameters
  • hooks (typing.Optional[collections.abc.MutableSet[SlashHooks]] = None): Set of hooks to include in this command execution.
Returns
  • typing.Optional[collections.abc.Awaitable[None]]: Awaitable used to wait for the command execution to finish.

This may be awaited or left to run as a background task.

If this is None then the client should carry on its search for a component with a matching command.

#   async def execute_message( self, ctx: tanjun.abc.MessageContext, /, *, hooks: Optional[collections.abc.MutableSet[tanjun.abc.Hooks[tanjun.abc.MessageContext]]] = None ) -> bool:
View Source
    async def execute_message(
        self,
        ctx: tanjun_abc.MessageContext,
        /,
        *,
        hooks: typing.Optional[collections.MutableSet[tanjun_abc.MessageHooks]] = None,
    ) -> bool:
        # <<inherited docstring from tanjun.abc.Component>>.
        async for name, command in self._check_message_context(ctx):
            ctx.set_triggering_name(name)
            ctx.set_content(ctx.content[len(name) :].lstrip())
            ctx.set_component(self)
            # Only add our hooks if we're sure we'll be executing the command here.

            if self._message_hooks:
                if hooks is None:
                    hooks = set()

                hooks.add(self._message_hooks)

            if self._hooks:
                if hooks is None:
                    hooks = set()

                hooks.add(self._hooks)

            await command.execute(ctx, hooks=hooks)
            return True

        ctx.set_component(None)
        return False

Execute a message context.

Parameters
  • ctx (MessageContext): The context to execute.
Other Parameters
  • hooks (typing.Optional[collections.abc.MutableSet[MessageHooks]] = None): Set of hooks to include in this command execution.
Returns
  • bool: Whether a message command was executed in this component with the provided context.

If False then the client should carry on its search for a component with a matching command.

Raises
#   def add_schedule( self: ~_ComponentT, schedule: tanjun.schedules.AbstractSchedule, / ) -> ~_ComponentT:
View Source
    def add_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT:
        """Add a schedule to the component.

        Parameters
        ----------
        schedule : tanjun.schedules.AbstractSchedule
            The schedule to add.

        Returns
        -------
        Self
            The component itself for chaining.
        """
        if self._client and self._loop:
            # TODO: upgrade this to the standard interface
            assert isinstance(self._client, injecting.InjectorClient)
            schedule.start(self._client, loop=self._loop)

        self._schedules.append(schedule)
        return self

Add a schedule to the component.

Parameters
Returns
  • Self: The component itself for chaining.
#   def remove_schedule( self: ~_ComponentT, schedule: tanjun.schedules.AbstractSchedule, / ) -> ~_ComponentT:
View Source
    def remove_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT:
        """Remove a schedule from the component.

        Parameters
        ----------
        schedule : tanjun.schedules.AbstractSchedule
            The schedule to remove

        Returns
        -------
        Self
            The component itself for chaining.

        Raises
        ------
        ValueError
            If the schedule isn't registered.
        """
        if schedule.is_alive:
            schedule.stop()

        self._schedules.remove(schedule)
        return self

Remove a schedule from the component.

Parameters
Returns
  • Self: The component itself for chaining.
Raises
  • ValueError: If the schedule isn't registered.
#   def with_schedule(self, schedule: ~_ScheduleT, /) -> ~_ScheduleT:
View Source
    def with_schedule(self, schedule: _ScheduleT, /) -> _ScheduleT:
        """Add a schedule to the component through a decorator call.

        Example
        -------
        This may be used in conjunction with `tanjun.as_interval`.

        ```py
        @component.with_schedule
        @tanjun.as_interval(60)
        async def my_schedule():
            print("I'm running every minute!")
        ```

        Parameters
        ----------
        schedule : schedules.AbstractSchedule
            The schedule to add.

        Returns
        -------
        schedules.AbstractSchedule
            The added schedule.
        """
        self.add_schedule(schedule)
        return schedule

Add a schedule to the component through a decorator call.

Example

This may be used in conjunction with tanjun.as_interval.

@component.with_schedule
@tanjun.as_interval(60)
async def my_schedule():
    print("I'm running every minute!")
Parameters
  • schedule (schedules.AbstractSchedule): The schedule to add.
Returns
  • schedules.AbstractSchedule: The added schedule.
#   async def close(self, *, unbind: bool = False) -> None:
View Source
    async def close(self, *, unbind: bool = False) -> None:
        # <<inherited docstring from tanjun.abc.Component>>.
        if not self._loop:
            raise RuntimeError("Component isn't active")

        assert self._client

        for schedule in self._schedules:
            if schedule.is_alive:
                schedule.stop()

        self._loop = None
        # TODO: upgrade this to the standard interface
        assert isinstance(self._client, injecting.InjectorClient)
        await asyncio.gather(
            *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_close)
        )
        if unbind:
            self.unbind_client(self._client)

Close the component.

Other Parameters
  • unbind (bool): Whether to unbind from the client after this is closed.

    Defaults to False.

Raises
  • RuntimeError: If the component isn't running.
#   async def open(self) -> None:
View Source
    async def open(self) -> None:
        # <<inherited docstring from tanjun.abc.Component>>.
        if self._loop:
            raise RuntimeError("Component is already active")

        if not self._client:
            raise RuntimeError("Client isn't bound yet")

        self._loop = asyncio.get_running_loop()
        # TODO: upgrade this to the standard interface
        assert isinstance(self._client, injecting.InjectorClient)
        await asyncio.gather(
            *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_open)
        )

        for schedule in self._schedules:
            schedule.start(self._client, loop=self._loop)

Start the component.

Raises
  • RuntimeError: If the component is already open. If the component isn't bound to a client.
#   def make_loader(self, *, copy: bool = True) -> tanjun.abc.ClientLoader:
View Source
    def make_loader(self, *, copy: bool = True) -> tanjun_abc.ClientLoader:
        """Make a loader/unloader for this component.

        This enables loading, unloading and reloading of this component into a
        client by targeting the module using `tanjun.Client.load_modules`,
        `tanjun.Client.unload_modules` and `tanjun.Client.reload_modules`.

        Other Parameters
        ----------------
        copy: bool
            Whether to copy the component before loading it into a client.

            Defaults to `True`.

        Returns
        -------
        tanjun.abc.ClientLoader
            The loader for this component.
        """
        return _ComponentManager(self, copy)

Make a loader/unloader for this component.

This enables loading, unloading and reloading of this component into a client by targeting the module using tanjun.Client.load_modules, tanjun.Client.unload_modules and tanjun.Client.reload_modules.

Other Parameters
  • copy (bool): Whether to copy the component before loading it into a client.

    Defaults to True.

Returns
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Standard command execution context implementations."""
from __future__ import annotations

__all__: list[str] = ["MessageContext", "ResponseTypeT", "SlashContext", "SlashOption"]

import asyncio
import datetime
import logging
import typing

import hikari
from hikari import snowflakes

from . import abc as tanjun_abc
from . import injecting

if typing.TYPE_CHECKING:
    from collections import abc as collections

    from hikari import traits as hikari_traits

    _BaseContextT = typing.TypeVar("_BaseContextT", bound="BaseContext")
    _MessageContextT = typing.TypeVar("_MessageContextT", bound="MessageContext")
    _SlashContextT = typing.TypeVar("_SlashContextT", bound="SlashContext")
    _T = typing.TypeVar("_T")

ResponseTypeT = typing.Union[hikari.api.InteractionMessageBuilder, hikari.api.InteractionDeferredBuilder]
"""Union of the response types which are valid for application command interactions."""
_INTERACTION_LIFETIME: typing.Final[datetime.timedelta] = datetime.timedelta(minutes=15)
_LOGGER = logging.getLogger("hikari.tanjun.context")


def _delete_after_to_float(delete_after: typing.Union[datetime.timedelta, float, int]) -> float:
    return delete_after.total_seconds() if isinstance(delete_after, datetime.timedelta) else float(delete_after)


class BaseContext(injecting.BasicInjectionContext, tanjun_abc.Context):
    """Base class for all standard context implementations."""

    __slots__ = ("_client", "_component", "_final")

    def __init__(
        self,
        client: tanjun_abc.Client,
        injection_client: injecting.InjectorClient,
        *,
        component: typing.Optional[tanjun_abc.Component] = None,
    ) -> None:
        # injecting.BasicInjectionContext.__init__
        super().__init__(injection_client)
        self._client = client
        self._component = component
        self._final = False
        (
            self._set_type_special_case(tanjun_abc.Context, self)
            ._set_type_special_case(BaseContext, self)
            ._set_type_special_case(type(self), self)
        )

    @property
    def cache(self) -> typing.Optional[hikari.api.Cache]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client.cache

    @property
    def client(self) -> tanjun_abc.Client:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client

    @property
    def component(self) -> typing.Optional[tanjun_abc.Component]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._component

    @property
    def events(self) -> typing.Optional[hikari.api.EventManager]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client.events

    @property
    def server(self) -> typing.Optional[hikari.api.InteractionServer]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client.server

    @property
    def rest(self) -> hikari.api.RESTClient:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client.rest

    @property
    def shards(self) -> typing.Optional[hikari_traits.ShardAware]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client.shards

    @property
    def voice(self) -> typing.Optional[hikari.api.VoiceComponent]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client.voice

    def _assert_not_final(self) -> None:
        if self._final:
            raise TypeError("Cannot modify a finalised context")

    def finalise(self: _BaseContextT) -> _BaseContextT:
        """Finalise the context, dis-allowing any further modifications.

        Returns
        -------
        Self
            The context itself to enable chained calls.
        """
        self._final = True
        return self

    def set_component(self: _BaseContextT, component: typing.Optional[tanjun_abc.Component], /) -> _BaseContextT:
        # <<inherited docstring from tanjun.abc.Context>>.
        self._assert_not_final()
        if component:
            self._set_type_special_case(tanjun_abc.Component, component)._set_type_special_case(
                type(component), component
            )

        elif component_case := self._special_case_types.get(tanjun_abc.Component):
            self._remove_type_special_case(tanjun_abc.Component)
            self._remove_type_special_case(type(component_case))

        self._component = component
        return self

    def get_channel(self) -> typing.Optional[hikari.TextableGuildChannel]:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._client.cache:
            channel = self._client.cache.get_guild_channel(self.channel_id)
            assert channel is None or isinstance(channel, hikari.TextableGuildChannel)
            return channel

        return None

    def get_guild(self) -> typing.Optional[hikari.Guild]:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self.guild_id is not None and self._client.cache:
            return self._client.cache.get_guild(self.guild_id)

        return None

    async def fetch_channel(self) -> hikari.TextableChannel:
        # <<inherited docstring from tanjun.abc.Context>>.
        channel = await self._client.rest.fetch_channel(self.channel_id)
        assert isinstance(channel, hikari.TextableChannel)
        return channel

    async def fetch_guild(self) -> typing.Optional[hikari.Guild]:  # TODO: or raise?
        # <<inherited docstring from tanjun.abc.Context>>.
        if self.guild_id is not None:
            return await self._client.rest.fetch_guild(self.guild_id)

        return None


class MessageContext(BaseContext, tanjun_abc.MessageContext):
    """Standard implementation of a command context as used within Tanjun."""

    __slots__ = (
        "_command",
        "_content",
        "_initial_response_id",
        "_last_response_id",
        "_response_lock",
        "_message",
        "_triggering_name",
        "_triggering_prefix",
    )

    def __init__(
        self,
        client: tanjun_abc.Client,
        injection_client: injecting.InjectorClient,
        content: str,
        message: hikari.Message,
        *,
        command: typing.Optional[tanjun_abc.MessageCommand[typing.Any]] = None,
        component: typing.Optional[tanjun_abc.Component] = None,
        triggering_name: str = "",
        triggering_prefix: str = "",
    ) -> None:
        if message.content is None:
            raise ValueError("Cannot spawn context with a content-less message.")

        super().__init__(client, injection_client, component=component)
        self._command = command
        self._content = content
        self._initial_response_id: typing.Optional[hikari.Snowflake] = None
        self._last_response_id: typing.Optional[hikari.Snowflake] = None
        self._response_lock = asyncio.Lock()
        self._message = message
        self._triggering_name = triggering_name
        self._triggering_prefix = triggering_prefix
        self._set_type_special_case(tanjun_abc.MessageContext, self)._set_type_special_case(MessageContext, self)

    def __repr__(self) -> str:
        return f"MessageContext <{self._message!r}, {self._command!r}>"

    @property
    def author(self) -> hikari.User:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._message.author

    @property
    def channel_id(self) -> hikari.Snowflake:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._message.channel_id

    @property
    def command(self) -> typing.Optional[tanjun_abc.MessageCommand[typing.Any]]:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        return self._command

    @property
    def content(self) -> str:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        return self._content

    @property
    def created_at(self) -> datetime.datetime:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._message.created_at

    @property
    def guild_id(self) -> typing.Optional[hikari.Snowflake]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._message.guild_id

    @property
    def has_responded(self) -> bool:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._initial_response_id is not None

    @property
    def is_human(self) -> bool:
        # <<inherited docstring from tanjun.abc.Context>>.
        return not self._message.author.is_bot and self._message.webhook_id is None

    @property
    def member(self) -> typing.Optional[hikari.Member]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._message.member

    @property
    def message(self) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        return self._message

    @property
    def triggering_name(self) -> str:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._triggering_name

    @property
    def triggering_prefix(self) -> str:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        return self._triggering_prefix

    @property
    def shard(self) -> typing.Optional[hikari.api.GatewayShard]:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        if not self._client.shards:
            return None

        if self._message.guild_id is not None:
            shard_id = snowflakes.calculate_shard_id(self._client.shards, self._message.guild_id)

        else:
            shard_id = 0

        return self._client.shards.shards[shard_id]

    def set_command(
        self: _MessageContextT, command: typing.Optional[tanjun_abc.MessageCommand[typing.Any]], /
    ) -> _MessageContextT:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        self._assert_not_final()
        self._command = command
        if command:
            (
                self._set_type_special_case(tanjun_abc.ExecutableCommand, command)
                ._set_type_special_case(tanjun_abc.MessageCommand, command)
                ._set_type_special_case(type(command), command)
            )

        elif command_case := self._special_case_types.get(tanjun_abc.ExecutableCommand):
            self._remove_type_special_case(tanjun_abc.ExecutableCommand)
            self._remove_type_special_case(tanjun_abc.MessageCommand)  # TODO: command group?
            self._remove_type_special_case(type(command_case))

        return self

    def set_content(self: _MessageContextT, content: str, /) -> _MessageContextT:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        self._assert_not_final()
        self._content = content
        return self

    def set_triggering_name(self: _MessageContextT, name: str, /) -> _MessageContextT:
        # <<inherited docstring from tanjun.abc.MessageContext>>.
        self._assert_not_final()
        self._triggering_name = name
        return self

    def set_triggering_prefix(self: _MessageContextT, triggering_prefix: str, /) -> _MessageContextT:
        """Set the triggering prefix for this context.

        Parameters
        ----------
        triggering_prefix : str
            The triggering prefix to set.

        Returns
        -------
        Self
            This context to allow for chaining.
        """
        self._assert_not_final()
        self._triggering_prefix = triggering_prefix
        return self

    async def delete_initial_response(self) -> None:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._initial_response_id is None:
            raise LookupError("Context has no initial response")

        await self._client.rest.delete_message(self._message.channel_id, self._initial_response_id)

    async def delete_last_response(self) -> None:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._last_response_id is None:
            raise LookupError("Context has no previous responses")

        await self._client.rest.delete_message(self._message.channel_id, self._last_response_id)

    async def edit_initial_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        replace_attachments: bool = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        delete_after = _delete_after_to_float(delete_after) if delete_after is not None else None
        if self._initial_response_id is None:
            raise LookupError("Context has no initial response")

        message = await self.rest.edit_message(
            self._message.channel_id,
            self._initial_response_id,
            content=content,
            attachment=attachment,
            attachments=attachments,
            component=component,
            components=components,
            embed=embed,
            embeds=embeds,
            replace_attachments=replace_attachments,
            mentions_everyone=mentions_everyone,
            user_mentions=user_mentions,
            role_mentions=role_mentions,
        )
        if delete_after is not None:
            asyncio.create_task(self._delete_after(delete_after, message))

        return message

    async def edit_last_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        replace_attachments: bool = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        delete_after = _delete_after_to_float(delete_after) if delete_after is not None else None
        if self._last_response_id is None:
            raise LookupError("Context has no previous tracked response")

        message = await self.rest.edit_message(
            self._message.channel_id,
            self._last_response_id,
            content=content,
            attachment=attachment,
            attachments=attachments,
            component=component,
            components=components,
            embed=embed,
            embeds=embeds,
            replace_attachments=replace_attachments,
            mentions_everyone=mentions_everyone,
            user_mentions=user_mentions,
            role_mentions=role_mentions,
        )

        if delete_after is not None:
            asyncio.create_task(self._delete_after(delete_after, message))

        return message

    async def fetch_initial_response(self) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._initial_response_id is not None:
            return await self.client.rest.fetch_message(self._message.channel_id, self._initial_response_id)

        raise LookupError("No initial response found for this context")

    async def fetch_last_response(self) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._last_response_id is not None:
            return await self.client.rest.fetch_message(self._message.channel_id, self._last_response_id)

        raise LookupError("No responses found for this context")

    @staticmethod
    async def _delete_after(delete_after: float, message: hikari.Message) -> None:
        await asyncio.sleep(delete_after)
        try:
            await message.delete()
        except hikari.NotFoundError as exc:
            _LOGGER.debug("Failed to delete response message after %.2f seconds", delete_after, exc_info=exc)

    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: bool = True,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        nonce: hikari.UndefinedOr[str] = hikari.UNDEFINED,
        reply: typing.Union[bool, hikari.SnowflakeishOr[hikari.PartialMessage], hikari.UndefinedType] = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        mentions_reply: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        delete_after = _delete_after_to_float(delete_after) if delete_after is not None else None
        async with self._response_lock:
            message = await self._message.respond(
                content=content,
                attachment=attachment,
                attachments=attachments,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                tts=tts,
                nonce=nonce,
                reply=reply,
                mentions_everyone=mentions_everyone,
                mentions_reply=mentions_reply,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
            )
            self._last_response_id = message.id
            if self._initial_response_id is None:
                self._initial_response_id = message.id

            if delete_after is not None:
                asyncio.create_task(self._delete_after(delete_after, message))

            return message


_SnowflakeOptions = {
    hikari.OptionType.USER,
    hikari.OptionType.MENTIONABLE,
    hikari.OptionType.ROLE,
    hikari.OptionType.CHANNEL,
}


class SlashOption(tanjun_abc.SlashOption):
    __slots__ = ("_interaction", "_option")

    def __init__(self, interaction: hikari.CommandInteraction, option: hikari.CommandInteractionOption, /):
        if option.value is None:
            raise ValueError("Cannot build a slash option with a value-less API representation")

        self._interaction = interaction
        self._option = option

    @property
    def name(self) -> str:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        return self._option.name

    @property
    def type(self) -> typing.Union[hikari.OptionType, int]:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        return self._option.type

    @property
    def value(self) -> typing.Union[str, int, hikari.Snowflake, bool, float]:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        # This is asserted in __init__
        assert self._option.value is not None
        if self._option.type in _SnowflakeOptions:
            assert self._option.value is not None
            return hikari.Snowflake(self._option.value)

        return self._option.value

    def boolean(self) -> bool:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self.type is hikari.OptionType.BOOLEAN:
            return bool(self._option.value)

        raise TypeError("Option is not a boolean")

    def float(self) -> float:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self.type is hikari.OptionType.FLOAT:
            assert self._option.value is not None
            return float(self._option.value)

        raise TypeError("Option is not a float")

    def integer(self) -> int:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self.type is hikari.OptionType.INTEGER:
            assert self._option.value is not None
            return int(self._option.value)

        raise TypeError("Option is not an integer")

    def snowflake(self) -> hikari.Snowflake:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self.type in _SnowflakeOptions:
            assert self._option.value is not None
            return hikari.Snowflake(self._option.value)

        raise TypeError("Option is not a unique resource")

    def string(self) -> str:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self.type is hikari.OptionType.STRING:
            return str(self._option.value)

        raise TypeError("Option is not a string")

    def resolve_value(
        self,
    ) -> typing.Union[hikari.InteractionChannel, hikari.InteractionMember, hikari.Role, hikari.User]:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self._option.type is hikari.OptionType.CHANNEL:
            return self.resolve_to_channel()

        if self._option.type is hikari.OptionType.ROLE:
            return self.resolve_to_role()

        if self._option.type is hikari.OptionType.USER:
            return self.resolve_to_user()

        if self._option.type is hikari.OptionType.MENTIONABLE:
            return self.resolve_to_mentionable()

        raise TypeError(f"Option type {self._option.type} isn't resolvable")

    def resolve_to_channel(self) -> hikari.InteractionChannel:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        # What does self.value being None mean?
        if self._option.type is hikari.OptionType.CHANNEL:
            assert self._interaction.resolved
            assert self._option.value is not None
            return self._interaction.resolved.channels[hikari.Snowflake(self._option.value)]

        raise TypeError(f"Cannot resolve non-channel option type {self._option.type} to a user")

    @typing.overload
    def resolve_to_member(self) -> hikari.InteractionMember:
        ...

    @typing.overload
    def resolve_to_member(self, *, default: _T) -> typing.Union[hikari.InteractionMember, _T]:
        ...

    def resolve_to_member(self, *, default: _T = ...) -> typing.Union[hikari.InteractionMember, _T]:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        # What does self.value being None mean?
        if self._option.type is hikari.OptionType.USER:
            assert self._interaction.resolved
            assert self._option.value is not None
            if member := self._interaction.resolved.members.get(hikari.Snowflake(self._option.value)):
                return member

            if default is not ...:
                return default

            raise LookupError("User isn't in the current guild") from None

        if self._option.type is hikari.OptionType.MENTIONABLE:
            assert self._option.value is not None
            assert self._interaction.resolved
            target_id = hikari.Snowflake(self._option.value)
            if member := self._interaction.resolved.members.get(target_id):
                return member

            if target_id in self._interaction.resolved.users:
                if default is not ...:
                    return default

                raise LookupError("User isn't in the current guild")

        raise TypeError(f"Cannot resolve non-user option type {self._option.type} to a member")

    def resolve_to_mentionable(self) -> typing.Union[hikari.Role, hikari.User, hikari.Member]:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self._option.type is hikari.OptionType.MENTIONABLE:
            assert self._option.value is not None
            assert self._interaction.resolved
            target_id = hikari.Snowflake(self._option.value)
            if role := self._interaction.resolved.roles.get(target_id):
                return role

            return self._interaction.resolved.members.get(target_id) or self._interaction.resolved.users[target_id]

        if self._option.type is hikari.OptionType.USER:
            return self.resolve_to_user()

        if self._option.type is hikari.OptionType.ROLE:
            return self.resolve_to_role()

        raise TypeError(f"Cannot resolve non-mentionable option type {self._option.type} to a mentionable entity.")

    def resolve_to_role(self) -> hikari.Role:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self._option.type is hikari.OptionType.ROLE:
            assert self._interaction.resolved
            assert self._option.value is not None
            return self._interaction.resolved.roles[hikari.Snowflake(self._option.value)]

        if self._option.type is hikari.OptionType.MENTIONABLE:
            assert self._interaction.resolved
            if role := self._interaction.resolved.roles.get(hikari.Snowflake(self.value)):
                return role

        raise TypeError(f"Cannot resolve non-role option type {self._option.type} to a role")

    def resolve_to_user(self) -> typing.Union[hikari.User, hikari.Member]:
        # <<inherited docstring from tanjun.abc.SlashOption>>.
        if self._option.type is hikari.OptionType.USER:
            assert self._interaction.resolved
            assert self._option.value is not None
            user_id = hikari.Snowflake(self._option.value)
            return self._interaction.resolved.members.get(user_id) or self._interaction.resolved.users[user_id]

        if self._option.type is hikari.OptionType.MENTIONABLE:
            assert self._interaction.resolved
            assert self._option.value is not None
            user_id = hikari.Snowflake(self._option.value)
            if result := self._interaction.resolved.members.get(user_id) or self._interaction.resolved.users.get(
                user_id
            ):
                return result

        raise TypeError(f"Cannot resolve non-user option type {self._option.type} to a user")


_COMMAND_OPTION_TYPES: typing.Final[frozenset[hikari.OptionType]] = frozenset(
    [hikari.OptionType.SUB_COMMAND, hikari.OptionType.SUB_COMMAND_GROUP]
)


class SlashContext(BaseContext, tanjun_abc.SlashContext):
    __slots__ = (
        "_command",
        "_defaults_to_ephemeral",
        "_defer_task",
        "_has_been_deferred",
        "_has_responded",
        "_interaction",
        "_last_response_id",
        "_marked_not_found",
        "_on_not_found",
        "_options",
        "_response_future",
        "_response_lock",
    )

    def __init__(
        self,
        client: tanjun_abc.Client,
        injection_client: injecting.InjectorClient,
        interaction: hikari.CommandInteraction,
        *,
        command: typing.Optional[tanjun_abc.BaseSlashCommand] = None,
        component: typing.Optional[tanjun_abc.Component] = None,
        default_to_ephemeral: bool = False,
        on_not_found: typing.Optional[collections.Callable[[SlashContext], collections.Awaitable[None]]] = None,
    ) -> None:
        super().__init__(client, injection_client, component=component)
        self._command = command
        self._defaults_to_ephemeral = default_to_ephemeral
        self._defer_task: typing.Optional[asyncio.Task[None]] = None
        self._has_been_deferred = False
        self._has_responded = False
        self._interaction = interaction
        self._last_response_id: typing.Optional[hikari.Snowflake] = None
        self._marked_not_found = False
        self._on_not_found = on_not_found
        self._response_future: typing.Optional[asyncio.Future[ResponseTypeT]] = None
        self._response_lock = asyncio.Lock()
        self._set_type_special_case(tanjun_abc.SlashContext, self)._set_type_special_case(SlashContext, self)

        options = interaction.options
        while options and (first_option := options[0]).type in _COMMAND_OPTION_TYPES:
            options = first_option.options

        if options:
            self._options = {option.name: SlashOption(interaction, option) for option in options}

        else:
            self._options = {}

    @property
    def author(self) -> hikari.User:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._interaction.user

    @property
    def channel_id(self) -> hikari.Snowflake:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._interaction.channel_id

    @property
    def client(self) -> tanjun_abc.Client:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._client

    @property
    def command(self) -> typing.Optional[tanjun_abc.BaseSlashCommand]:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        return self._command

    @property
    def created_at(self) -> datetime.datetime:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._interaction.created_at

    @property
    def defaults_to_ephemeral(self) -> bool:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._defaults_to_ephemeral

    @property
    def expires_at(self) -> datetime.datetime:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        return self.created_at + _INTERACTION_LIFETIME

    @property
    def guild_id(self) -> typing.Optional[hikari.Snowflake]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._interaction.guild_id

    @property
    def has_been_deferred(self) -> bool:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        return self._has_been_deferred

    @property
    def has_responded(self) -> bool:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._has_responded

    @property
    def is_human(self) -> typing.Literal[True]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return True

    @property
    def member(self) -> typing.Optional[hikari.InteractionMember]:
        # <<inherited docstring from tanjun.abc.Context>>.
        return self._interaction.member

    @property
    def triggering_name(self) -> str:
        # <<inherited docstring from tanjun.abc.Context>>.
        # TODO: account for command groups
        return self._interaction.command_name

    @property
    def interaction(self) -> hikari.CommandInteraction:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        return self._interaction

    @property
    def options(self) -> collections.Mapping[str, tanjun_abc.SlashOption]:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        return self._options.copy()

    async def _auto_defer(self, countdown: typing.Union[int, float], /) -> None:
        await asyncio.sleep(countdown)
        await self.defer()

    def cancel_defer(self) -> None:
        """Cancel the auto-deferral if its active."""
        if self._defer_task:
            self._defer_task.cancel()

    def _get_flags(
        self, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED
    ) -> typing.Union[int, hikari.MessageFlag]:
        if flags is hikari.UNDEFINED:
            return hikari.MessageFlag.EPHEMERAL if self._defaults_to_ephemeral else hikari.MessageFlag.NONE

        return flags or hikari.MessageFlag.NONE

    def get_response_future(self) -> asyncio.Future[ResponseTypeT]:
        """Get the future which will be used to set the initial response.

        .. note::
            This will change the behaviour of this context to match the
            REST server flow.

        Returns
        -------
        asyncio.Future[ResponseTypeT]
            The future which will be used to set the initial response.
        """
        if not self._response_future:
            self._response_future = asyncio.get_running_loop().create_future()

        return self._response_future

    async def mark_not_found(self) -> None:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        # TODO: assert not finalised?
        if self._on_not_found and not self._marked_not_found:
            self._marked_not_found = True
            await self._on_not_found(self)

    def start_defer_timer(self: _SlashContextT, count_down: typing.Union[int, float], /) -> _SlashContextT:
        """Start the auto-deferral timer.

        Parameters
        ----------
        count_down : typing.Union[int, float]
            The number of seconds to wait before automatically deferring the
            interaction.

        Returns
        -------
        Self
            This context to allow for chaining.
        """
        self._assert_not_final()
        if self._defer_task:
            raise RuntimeError("Defer timer already set")

        self._defer_task = asyncio.create_task(self._auto_defer(count_down))
        return self

    def set_command(self: _SlashContextT, command: typing.Optional[tanjun_abc.BaseSlashCommand], /) -> _SlashContextT:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        self._assert_not_final()
        self._command = command
        if command:
            (
                self._set_type_special_case(tanjun_abc.ExecutableCommand, command)
                ._set_type_special_case(tanjun_abc.BaseSlashCommand, command)
                ._set_type_special_case(tanjun_abc.SlashCommand, command)
                ._set_type_special_case(type(command), command)
            )
        elif command_case := self._special_case_types.get(tanjun_abc.ExecutableCommand):
            self._remove_type_special_case(tanjun_abc.ExecutableCommand)
            self._remove_type_special_case(tanjun_abc.BaseSlashCommand)
            self._remove_type_special_case(tanjun_abc.SlashCommand)  # TODO: command group?
            self._remove_type_special_case(type(command_case))

        return self

    def set_ephemeral_default(self: _SlashContextT, state: bool, /) -> _SlashContextT:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        self._assert_not_final()  # TODO: document not final assertions.
        self._defaults_to_ephemeral = state
        return self

    async def defer(
        self, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED
    ) -> None:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        flags = self._get_flags(flags)
        in_defer_task = self._defer_task and self._defer_task is asyncio.current_task()
        if not in_defer_task:
            self.cancel_defer()

        async with self._response_lock:
            if self._has_been_deferred:
                if in_defer_task:
                    return

                raise RuntimeError("Context has already been responded to")

            self._has_been_deferred = True
            if self._response_future:
                self._response_future.set_result(self._interaction.build_deferred_response().set_flags(flags))

            else:
                await self._interaction.create_initial_response(
                    hikari.ResponseType.DEFERRED_MESSAGE_CREATE, flags=flags
                )

    def _validate_delete_after(self, delete_after: typing.Union[float, int, datetime.timedelta]) -> float:
        delete_after = _delete_after_to_float(delete_after)
        time_left = (
            _INTERACTION_LIFETIME - (datetime.datetime.now(tz=datetime.timezone.utc) - self.created_at)
        ).total_seconds()
        if delete_after + 10 > time_left:
            raise ValueError("This interaction will have expired before delete_after is reached")

        return delete_after

    async def _delete_followup_after(self, delete_after: float, message: hikari.Message) -> None:
        await asyncio.sleep(delete_after)
        try:
            await self._interaction.delete_message(message)
        except hikari.NotFoundError as exc:
            _LOGGER.debug("Failed to delete response message after %.2f seconds", delete_after, exc_info=exc)

    async def _create_followup(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED,
    ) -> hikari.Message:
        delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None
        message = await self._interaction.execute(
            content=content,
            attachment=attachment,
            attachments=attachments,
            component=component,
            components=components,
            embed=embed,
            embeds=embeds,
            flags=self._get_flags(flags),
            tts=tts,
            mentions_everyone=mentions_everyone,
            user_mentions=user_mentions,
            role_mentions=role_mentions,
        )
        self._last_response_id = message.id
        # This behaviour is undocumented and only kept by Discord for "backwards compatibility"
        # but the followup endpoint can be used to create the initial response for slash
        # commands or edit in a deferred response and (while this does lead to some
        # unexpected behaviour around deferrals) should be accounted for.
        if not self._has_responded:
            self._has_responded = True

        if delete_after is not None and not message.flags & hikari.MessageFlag.EPHEMERAL:
            asyncio.create_task(self._delete_followup_after(delete_after, message))

        return message

    async def create_followup(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED,
    ) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.SlashContext>>.
        async with self._response_lock:
            return await self._create_followup(
                content=content,
                delete_after=delete_after,
                attachment=attachment,
                attachments=attachments,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                mentions_everyone=mentions_everyone,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
                tts=tts,
                flags=flags,
            )

    async def _delete_initial_response_after(self, delete_after: float) -> None:
        await asyncio.sleep(delete_after)
        try:
            await self.delete_initial_response()
        except hikari.NotFoundError as exc:
            _LOGGER.debug("Failed to delete response message after %.2f seconds", delete_after, exc_info=exc)

    async def _create_initial_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
        flags: typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
    ) -> None:
        flags = self._get_flags(flags)
        delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None
        if self._has_responded:
            raise RuntimeError("Initial response has already been created")

        if self._has_been_deferred:
            raise RuntimeError(
                "edit_initial_response must be used to set the initial response after a context has been deferred"
            )

        self.cancel_defer()
        self._has_responded = True
        if not self._response_future:
            await self._interaction.create_initial_response(
                response_type=hikari.ResponseType.MESSAGE_CREATE,
                content=content,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                flags=flags,
                tts=tts,
                mentions_everyone=mentions_everyone,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
            )

        else:
            if component and components:
                raise ValueError("Only one of component or components may be passed")

            if embed and embeds:
                raise ValueError("Only one of embed or embeds may be passed")

            if component:
                assert not isinstance(component, hikari.UndefinedType)
                components = (component,)

            if embed:
                assert not isinstance(embed, hikari.UndefinedType)
                embeds = (embed,)

            content = str(content) if content is not hikari.UNDEFINED else hikari.UNDEFINED
            # Pyright doesn't properly support attrs and doesn't account for _ being removed from field
            # pre-fix in init.
            result = hikari.impl.InteractionMessageBuilder(
                type=hikari.ResponseType.MESSAGE_CREATE,  # type: ignore
                content=content,  # type: ignore
                components=components,  # type: ignore
                embeds=embeds,  # type: ignore
                flags=flags,  # type: ignore
                is_tts=tts,  # type: ignore
                mentions_everyone=mentions_everyone,  # type: ignore
                user_mentions=user_mentions,  # type: ignore
                role_mentions=role_mentions,  # type: ignore
            )  # type: ignore
            if embeds is not hikari.UNDEFINED:
                for embed in embeds:
                    result.add_embed(embed)

            self._response_future.set_result(result)

        if delete_after is not None and not flags & hikari.MessageFlag.EPHEMERAL:
            asyncio.create_task(self._delete_initial_response_after(delete_after))

    async def create_initial_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
        flags: typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] = hikari.UNDEFINED,
        tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Context>>.
        async with self._response_lock:
            await self._create_initial_response(
                delete_after=delete_after,
                content=content,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                mentions_everyone=mentions_everyone,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
                flags=flags,
                tts=tts,
            )

    async def delete_initial_response(self) -> None:
        # <<inherited docstring from tanjun.abc.Context>>.
        await self._interaction.delete_initial_response()
        # If they defer then delete the initial response then this should be treated as having
        # an initial response to allow for followup responses.
        self._has_responded = True

    async def delete_last_response(self) -> None:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._last_response_id is None:
            if self._has_responded or self._has_been_deferred:
                await self._interaction.delete_initial_response()
                # If they defer then delete the initial response then this should be treated as having
                # an initial response to allow for followup responses.
                self._has_responded = True
                return

            raise LookupError("Context has no last response")

        await self._interaction.delete_message(self._last_response_id)

    async def edit_initial_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        replace_attachments: bool = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None
        message = await self._interaction.edit_initial_response(
            content=content,
            attachment=attachment,
            attachments=attachments,
            component=component,
            components=components,
            embed=embed,
            embeds=embeds,
            replace_attachments=replace_attachments,
            mentions_everyone=mentions_everyone,
            user_mentions=user_mentions,
            role_mentions=role_mentions,
        )
        self._has_responded = True

        if delete_after is not None and not message.flags & hikari.MessageFlag.EPHEMERAL:
            asyncio.create_task(self._delete_initial_response_after(delete_after))

        return message

    async def edit_last_response(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
        attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
        component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        replace_attachments: bool = False,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._last_response_id:
            delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None
            message = await self._interaction.edit_message(
                self._last_response_id,
                content=content,
                attachment=attachment,
                attachments=attachments,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                replace_attachments=replace_attachments,
                mentions_everyone=mentions_everyone,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
            )
            if delete_after is not None and not message.flags & hikari.MessageFlag.EPHEMERAL:
                asyncio.create_task(self._delete_followup_after(delete_after, message))

            return message

        if self._has_responded or self._has_been_deferred:
            return await self.edit_initial_response(
                delete_after=delete_after,
                content=content,
                attachment=attachment,
                attachments=attachments,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                replace_attachments=replace_attachments,
                mentions_everyone=mentions_everyone,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
            )

        raise LookupError("Context has no previous responses")

    async def fetch_initial_response(self) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        return await self._interaction.fetch_initial_response()

    async def fetch_last_response(self) -> hikari.Message:
        # <<inherited docstring from tanjun.abc.Context>>.
        if self._last_response_id is not None:
            return await self._interaction.fetch_message(self._last_response_id)

        if self._has_responded:
            return await self.fetch_initial_response()

        raise LookupError("Context has no previous known responses")

    @typing.overload
    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: typing.Literal[False] = False,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> typing.Optional[hikari.Message]:
        ...

    @typing.overload
    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: typing.Literal[True],
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> hikari.Message:
        ...

    async def respond(
        self,
        content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED,
        *,
        ensure_result: bool = False,
        delete_after: typing.Union[datetime.timedelta, float, int, None] = None,
        component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
        components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
        embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
        embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED,
        mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
        user_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]
        ] = hikari.UNDEFINED,
        role_mentions: hikari.UndefinedOr[
            typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]
        ] = hikari.UNDEFINED,
    ) -> typing.Optional[hikari.Message]:
        # <<inherited docstring from tanjun.abc.Context>>.
        async with self._response_lock:
            if self._has_responded:
                return await self._create_followup(
                    content,
                    delete_after=delete_after,
                    component=component,
                    components=components,
                    embed=embed,
                    embeds=embeds,
                    mentions_everyone=mentions_everyone,
                    user_mentions=user_mentions,
                    role_mentions=role_mentions,
                )

            if self._has_been_deferred:
                return await self.edit_initial_response(
                    delete_after=delete_after,
                    content=content,
                    component=component,
                    components=components,
                    embed=embed,
                    embeds=embeds,
                    mentions_everyone=mentions_everyone,
                    user_mentions=user_mentions,
                    role_mentions=role_mentions,
                )

            await self._create_initial_response(
                delete_after=delete_after,
                content=content,
                component=component,
                components=components,
                embed=embed,
                embeds=embeds,
                mentions_everyone=mentions_everyone,
                user_mentions=user_mentions,
                role_mentions=role_mentions,
            )

        if ensure_result:
            return await self._interaction.fetch_initial_response()

Standard command execution context implementations.

View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Functions and classes used to enable more Discord oriented argument converters."""
from __future__ import annotations

__all__: list[str] = [
    "from_datetime",
    "parse_snowflake",
    "parse_channel_id",
    "parse_emoji_id",
    "parse_role_id",
    "parse_user_id",
    "search_snowflakes",
    "search_channel_ids",
    "search_emoji_ids",
    "search_role_ids",
    "search_user_ids",
    "to_bool",
    "to_channel",
    "to_color",
    "to_colour",
    "to_datetime",
    "to_emoji",
    "to_guild",
    "to_invite",
    "to_invite_with_metadata",
    "to_member",
    "to_presence",
    "to_role",
    "to_snowflake",
    "to_user",
    "to_voice_state",
    "ToChannel",
    "ToEmoji",
    "ToGuild",
    "ToInvite",
    "ToInviteWithMetadata",
    "ToMember",
    "ToPresence",
    "ToRole",
    "ToUser",
    "ToVoiceState",
]

import abc
import datetime
import logging
import operator
import re
import typing
import urllib.parse as urlparse
from collections import abc as collections

import hikari

from . import abc as tanjun_abc
from . import injecting
from .dependencies import async_cache

if typing.TYPE_CHECKING:
    from . import parsing

_ArgumentT = typing.Union[str, int, float]
_ValueT = typing.TypeVar("_ValueT")
_LOGGER = logging.getLogger("hikari.tanjun.conversion")


class BaseConverter(typing.Generic[_ValueT], abc.ABC):
    """Base class for the standard converters.

    .. warning::
        Inheriting from this is completely unnecessary and should be avoided
        for people using the library unless they know what they're doing.


    This is detail of the standard implementation and isn't guaranteed to work
    between implementations but will work for implementations which provide
    the standard dependency injection or special cased support for these.

    While it isn't necessary to subclass this to implement your own converters
    since dependency injection can be used to access fields like the current Context,
    this class introduces some niceties around stuff like state warnings.
    """

    __slots__ = ()
    __pdoc__: typing.ClassVar[dict[str, bool]] = {
        "async_cache": False,
        "cache_components": False,
        "intents": False,
        "requires_cache": False,
        "__pdoc__": False,
    }

    @property
    @abc.abstractmethod
    def async_caches(self) -> collections.Sequence[typing.Any]:
        """Collection of the asynchronous caches that this converter relies on.

        This will only be necessary if the suggested intents or cache_components
        aren't enabled for a converter which requires cache.
        """

    @property
    @abc.abstractmethod
    def cache_components(self) -> hikari.CacheComponents:
        """Cache component(s) the converter takes advantage of.

        .. note::
            Unless `BaseConverter.requires_cache` is `True`, these cache components
            aren't necessary but simply avoid the converter from falling back to
            REST requests.

        This will be `hikari.CacheComponents.NONE` if the converter doesn't
        make cache calls.
        """

    @property
    @abc.abstractmethod
    def intents(self) -> hikari.Intents:
        """Gateway intents this converter takes advantage of.

        .. note::
            This field is supplementary to `BaseConverter.cache_components` and
            is used to detect when the relevant component might not actually be
            being kept up to date or filled by gateway events.

            Unless `BaseConverter.requires_cache` is `True`, these intents being
            disabled won't stop this converter from working as it'll still fall
            back to REST requests.
        """

    @property
    @abc.abstractmethod
    def requires_cache(self) -> bool:
        """Whether this converter relies on the relevant cache stores to work.

        If this is `True` then this converter will not function properly
        in an environment `BaseConverter.intents` or `BaseConverter.cache_components`
        isn't satisfied and will never fallback to REST requests.
        """

    def check_client(self, client: tanjun_abc.Client, parent_name: str, /) -> None:
        """Check that this converter will work with the given client.

        This never raises any errors but simply warns the user if the converter
        is not compatible with the given client.

        Parameters
        ----------
        client : tanjun.abc.Client
            The client to check against.
        parent_name : str
            The name of the converter's parent, used for warning messages.
        """
        # TODO: upgrade this stuff to the standard interface
        assert isinstance(client, injecting.InjectorClient)
        if not client.cache or any(client.get_type_dependency(cls) is injecting.UNDEFINED for cls in self.async_caches):
            if self.requires_cache:
                _LOGGER.warning(
                    f"Converter {self!r} registered with {parent_name} will always fail with a stateless client.",
                )

            elif self.cache_components:
                _LOGGER.warning(
                    f"Converter {self!r} registered with {parent_name} may not perform optimally in a stateless client.",
                )

        # elif missing_components := (self.cache_components & ~client.cache.components):
        #     _LOGGER.warning(

        if client.shards and (missing_intents := self.intents & ~client.shards.intents):
            _LOGGER.warning(
                f"Converter {self!r} registered with {parent_name} may not perform as expected "
                f"without the following intents: {missing_intents}",
            )


_DmCacheT = typing.Optional[async_cache.SfCache[hikari.DMChannel]]
_GuildChannelCacheT = typing.Optional[async_cache.SfCache[hikari.PartialChannel]]


# TODO: GuildChannelConverter
class ToChannel(BaseConverter[hikari.PartialChannel]):
    """Standard converter for channels mentions/IDs.

    For a standard instance of this see `to_channel`.
    """

    __slots__ = ("_include_dms",)

    def __init__(self, *, include_dms: bool = True) -> None:
        """Initialise a to channel converter.

        Other Parameters
        ----------------
        include_dms : bool
            Whether to include DM channels in the results.

            May lead to a lot of extra fallbacks to REST requests if
            the client doesn't have a registered async cache for DMs.

            Defaults to `True`.
        """
        self._include_dms = include_dms

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_GuildChannelCacheT, _DmCacheT)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.GUILD_CHANNELS

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _GuildChannelCacheT = injecting.inject(type=_GuildChannelCacheT),
        dm_cache: _DmCacheT = injecting.inject(type=_DmCacheT),
    ) -> hikari.PartialChannel:
        channel_id = parse_channel_id(argument, message="No valid channel mention or ID found")
        if ctx.cache and (channel_ := ctx.cache.get_guild_channel(channel_id)):
            return channel_

        no_guild_channel = False
        if cache:
            try:
                return await cache.get(channel_id)

            except async_cache.EntryNotFound:
                if not self._include_dms:
                    raise ValueError("Couldn't find channel") from None

                no_guild_channel = True

            except async_cache.CacheMissError:
                pass

        if dm_cache and self._include_dms:
            try:
                return await dm_cache.get(channel_id)

            except async_cache.EntryNotFound:
                if no_guild_channel:
                    raise ValueError("Couldn't find channel") from None

            except async_cache.CacheMissError:
                pass

        try:
            channel = await ctx.rest.fetch_channel(channel_id)
            if self._include_dms or isinstance(channel, hikari.GuildChannel):
                return channel

        except hikari.NotFoundError:
            pass

        raise ValueError("Couldn't find channel")


ChannelConverter = ToChannel
"""Deprecated alias of `ToChannel`."""

_EmojiCacheT = typing.Optional[async_cache.SfCache[hikari.KnownCustomEmoji]]


class ToEmoji(BaseConverter[hikari.KnownCustomEmoji]):
    """Standard converter for custom emojis.

    For a standard instance of this see `to_emoji`.

    .. note::
        If you just want to convert inpute to a `hikari.Emoji`, `hikari.CustomEmoji`
        or `hikari.UnicodeEmoji` without making any cache or REST calls then you
        can just use the relevant `hikari.Emoji.parse`, `hikari.CustomEmoji.parse`
        or `hikari.UnicodeEmoji.parse` methods.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_EmojiCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.EMOJIS

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILD_EMOJIS | hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _EmojiCacheT = injecting.inject(type=_EmojiCacheT),
    ) -> hikari.KnownCustomEmoji:
        emoji_id = parse_emoji_id(argument, message="No valid emoji or emoji ID found")

        if ctx.cache and (emoji := ctx.cache.get_emoji(emoji_id)):
            return emoji

        if cache:
            try:
                return await cache.get(emoji_id)

            except async_cache.EntryNotFound:
                raise ValueError("Couldn't find emoji") from None

            except async_cache.CacheMissError:
                pass

        if ctx.guild_id:
            try:
                return await ctx.rest.fetch_emoji(ctx.guild_id, emoji_id)

            except hikari.NotFoundError:
                pass

        raise ValueError("Couldn't find emoji")


EmojiConverter = ToEmoji
"""Deprecated alias of `ToEmoji`."""


_GuildCacheT = typing.Optional[async_cache.SfCache[hikari.Guild]]


class ToGuild(BaseConverter[hikari.Guild]):
    """Stanard converter for guilds.

    For a standard instance of this see `to_guild`.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_GuildCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.GUILDS

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _GuildCacheT = injecting.inject(type=_GuildCacheT),
    ) -> hikari.Guild:
        guild_id = parse_snowflake(argument, message="No valid guild ID found")
        if ctx.cache and (guild := ctx.cache.get_guild(guild_id)):
            return guild

        if cache:
            try:
                return await cache.get(guild_id)

            except async_cache.EntryNotFound:
                raise ValueError("Couldn't find guild") from None

            except async_cache.CacheMissError:
                pass

        try:
            return await ctx.rest.fetch_guild(guild_id)

        except hikari.NotFoundError:
            pass

        raise ValueError("Couldn't find guild")


GuildConverter = ToGuild
"""Deprecated alias of `ToGuild`."""

_InviteCacheT = typing.Optional[async_cache.AsyncCache[str, hikari.InviteWithMetadata]]


class ToInvite(BaseConverter[hikari.Invite]):
    """Standard converter for invites."""

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_InviteCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.INVITES

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILD_INVITES

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _InviteCacheT = injecting.inject(type=_InviteCacheT),
    ) -> hikari.Invite:
        if not isinstance(argument, str):
            raise ValueError(f"`{argument}` is not a valid invite code")

        if ctx.cache and (invite := ctx.cache.get_invite(argument)):
            return invite

        if cache:
            try:
                return await cache.get(argument)

            except async_cache.EntryNotFound:
                raise ValueError("Couldn't find invite") from None

            except async_cache.CacheMissError:
                pass

        try:
            return await ctx.rest.fetch_invite(argument)
        except hikari.NotFoundError:
            pass

        raise ValueError("Couldn't find invite")


InviteConverter = ToInvite
"""Deprecated alias of `ToInvite`."""


class ToInviteWithMetadata(BaseConverter[hikari.InviteWithMetadata]):
    """Standard converter for invites with metadata.

    For a standard instance of this see `to_invite_with_metadata`.

    .. note::
        Unlike `InviteConverter`, this converter is cache dependent.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_InviteCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.INVITES

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILD_INVITES

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return True

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: typing.Optional[_InviteCacheT] = injecting.inject(type=_InviteCacheT),
    ) -> hikari.InviteWithMetadata:
        if not isinstance(argument, str):
            raise ValueError(f"`{argument}` is not a valid invite code")

        if ctx.cache and (invite := ctx.cache.get_invite(argument)):
            return invite

        if cache and (invite := await cache.get(argument)):
            return invite

        raise ValueError("Couldn't find invite")


InviteWithMetadataConverter = ToInviteWithMetadata
"""Deprecated alias of `ToInviteWithMetadata`."""


_MemberCacheT = typing.Optional[async_cache.SfGuildBound[hikari.Member]]


class ToMember(BaseConverter[hikari.Member]):
    """Standard converter for guild members.

    For a standard instance of this see `to_member`.

    This converter allows both mentions, raw IDs and partial usernames/nicknames
    and only works within a guild context.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_MemberCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.MEMBERS

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILD_MEMBERS | hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _MemberCacheT = injecting.inject(type=_MemberCacheT),
    ) -> hikari.Member:
        if ctx.guild_id is None:
            raise ValueError("Cannot get a member from a DM channel")

        try:
            user_id = parse_user_id(argument, message="No valid user mention or ID found")

        except ValueError:
            if isinstance(argument, str):
                try:
                    return (await ctx.rest.search_members(ctx.guild_id, argument))[0]

                except (hikari.NotFoundError, IndexError):
                    pass

        else:
            if ctx.cache and (member := ctx.cache.get_member(ctx.guild_id, user_id)):
                return member

            if cache:
                try:
                    return await cache.get_from_guild(ctx.guild_id, user_id)

                except async_cache.EntryNotFound:
                    raise ValueError("Couldn't find member in this guild") from None

                except async_cache.CacheMissError:
                    pass

            try:
                return await ctx.rest.fetch_member(ctx.guild_id, user_id)

            except hikari.NotFoundError:
                pass

        raise ValueError("Couldn't find member in this guild")


MemberConverter = ToMember
"""Deprecated alias of `ToMember`."""

_PresenceCacheT = typing.Optional[async_cache.SfGuildBound[hikari.MemberPresence]]


class ToPresence(BaseConverter[hikari.MemberPresence]):
    """Standard converter for presences.

    For a standard instance of this see `to_presence`.

    This converter is cache dependent and only works in a guild context.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_PresenceCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.PRESENCES

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILD_PRESENCES | hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return True

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _PresenceCacheT = injecting.inject(type=_PresenceCacheT),
    ) -> hikari.MemberPresence:
        if ctx.guild_id is None:
            raise ValueError("Cannot get a presence from a DM channel")

        user_id = parse_user_id(argument, message="No valid member mention or ID found")
        if ctx.cache and (presence := ctx.cache.get_presence(ctx.guild_id, user_id)):
            return presence

        if cache and (presence := await cache.get_from_guild(ctx.guild_id, user_id, default=None)):
            return presence

        raise ValueError("Couldn't find presence in current guild")


PresenceConverter = ToPresence
"""Deprecated alias of `ToPresence`."""

_RoleCacheT = typing.Optional[async_cache.SfCache[hikari.Role]]


class ToRole(BaseConverter[hikari.Role]):
    """Standard converter for guild roles.

    For a standard instance of this see `to_role`.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_RoleCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.ROLES

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _RoleCacheT = injecting.inject(type=_RoleCacheT),
    ) -> hikari.Role:
        role_id = parse_role_id(argument, message="No valid role mention or ID found")
        if ctx.cache and (role := ctx.cache.get_role(role_id)):
            return role

        if cache:
            try:
                return await cache.get(role_id)

            except async_cache.EntryNotFound:
                raise ValueError("Couldn't find role") from None

            except async_cache.CacheMissError:
                pass

        if ctx.guild_id:
            for role in await ctx.rest.fetch_roles(ctx.guild_id):
                if role.id == role_id:
                    return role

        raise ValueError("Couldn't find role")


RoleConverter = ToRole
"""Deprecated alias of `ToRole`."""

_UserCacheT = typing.Optional[async_cache.SfCache[hikari.User]]


class ToUser(BaseConverter[hikari.User]):
    """Standard converter for users.

    For a standard instance of this see `to_user`.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_UserCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.NONE

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILDS | hikari.Intents.GUILD_MEMBERS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return False

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _UserCacheT = injecting.inject(type=_UserCacheT),
    ) -> hikari.User:
        # TODO: search by name if this is a guild context
        user_id = parse_user_id(argument, message="No valid user mention or ID found")
        if ctx.cache and (user := ctx.cache.get_user(user_id)):
            return user

        if cache:
            try:
                return await cache.get(user_id)

            except async_cache.EntryNotFound:
                raise ValueError("Couldn't find user") from None

            except async_cache.CacheMissError:
                pass

        try:
            return await ctx.rest.fetch_user(user_id)

        except hikari.NotFoundError:
            pass

        raise ValueError("Couldn't find user")


UserConverter = ToUser
"""Deprecated alias of `ToUser`."""

_VoiceStateCacheT = typing.Optional[async_cache.SfGuildBound[hikari.VoiceState]]


class ToVoiceState(BaseConverter[hikari.VoiceState]):
    """Standard converter for voice states.

    For a standard instance of this see `to_voice_state`.

    .. note::
        This converter is cache dependent and only works in a guild context.
    """

    __slots__ = ()

    @property
    def async_caches(self) -> collections.Sequence[typing.Any]:
        # <<inherited docstring from BaseConverter>>.
        return (_VoiceStateCacheT,)

    @property
    def cache_components(self) -> hikari.CacheComponents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.CacheComponents.VOICE_STATES

    @property
    def intents(self) -> hikari.Intents:
        # <<inherited docstring from BaseConverter>>.
        return hikari.Intents.GUILD_VOICE_STATES | hikari.Intents.GUILDS

    @property
    def requires_cache(self) -> bool:
        # <<inherited docstring from BaseConverter>>.
        return True

    async def __call__(
        self,
        argument: _ArgumentT,
        /,
        ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context),
        cache: _VoiceStateCacheT = injecting.inject(type=_VoiceStateCacheT),
    ) -> hikari.VoiceState:
        if ctx.guild_id is None:
            raise ValueError("Cannot get a voice state from a DM channel")

        user_id = parse_user_id(argument, message="No valid user mention or ID found")

        if ctx.cache and (state := ctx.cache.get_voice_state(ctx.guild_id, user_id)):
            return state

        if cache and (state := await cache.get_from_guild(ctx.guild_id, user_id, default=None)):
            return state

        raise ValueError("Voice state couldn't be found for current guild")


VoiceStateConverter = ToVoiceState
"""Deprecated alias of `ToVoiceState`."""


class _IDMatcherSig(typing.Protocol):
    def __call__(self, value: _ArgumentT, /, *, message: str = "No valid mention or ID found") -> hikari.Snowflake:
        raise NotImplementedError


def _make_snowflake_parser(regex: re.Pattern[str], /) -> _IDMatcherSig:
    def parse(value: _ArgumentT, /, *, message: str = "No valid mention or ID found") -> hikari.Snowflake:
        """Parse a snowflake from a string or int value.

        .. note::
            This only allows the relevant entity's mention format if applicable.

        Parameters
        ----------
        value: typing.Union[str, int]
            The value to parse (this argument can only be passed positionally).

        Other Parameters
        ----------------
        message: str
            The error message to raise if the value cannot be parsed.

        Returns
        -------
        hikari.Snowflake
            The parsed snowflake.

        Raises
        ------
        ValueError
            If the value cannot be parsed.
        """
        result: typing.Optional[hikari.Snowflake] = None
        if isinstance(value, str):
            if value.isdigit():
                result = hikari.Snowflake(value)

            else:
                capture = next(regex.finditer(value), None)
                result = hikari.Snowflake(capture.groups()[0]) if capture else None

        else:
            try:
                # Technically passing a float here is invalid (typing wise)
                # but we handle that by catching TypeError
                result = hikari.Snowflake(operator.index(typing.cast(int, value)))

            except (TypeError, ValueError):
                pass

        # We should also range check the provided ID.
        if result is not None and _range_check(result):
            return result

        raise ValueError(message) from None

    return parse


_IDSearcherSig = collections.Callable[[_ArgumentT], collections.Iterator[hikari.Snowflake]]


def _range_check(snowflake: hikari.Snowflake, /) -> bool:
    return snowflake.min() <= snowflake <= snowflake.max()


def _make_snowflake_searcher(regex: re.Pattern[str], /) -> _IDSearcherSig:
    def parse(value: _ArgumentT, /) -> collections.Iterator[hikari.Snowflake]:
        """Iterate over the snowflakes in a string.

        .. note::
            This only allows the relevant entity's mention format if applicable.

        Parameters
        ----------
        value: typing.Union[str, int]
            The value to parse (this argument can only be passed positionally).

        Returns
        -------
        collections.abc.Iterator[hikari.Snowflake]
            An iterator over the IDs found in the string.
        """
        if isinstance(value, str):
            if value.isdigit() and _range_check(result := hikari.Snowflake(value)):
                yield result

            else:
                yield from filter(
                    _range_check, map(hikari.Snowflake, (match.groups()[0] for match in regex.finditer(value)))
                )

                yield from filter(_range_check, map(hikari.Snowflake, filter(str.isdigit, value.split())))

        else:
            try:
                # Technically passing a float here is invalid (typing wise)
                # but we handle that by catching TypeError
                result = hikari.Snowflake(operator.index(typing.cast(int, value)))

            except (TypeError, ValueError):
                pass

            else:
                if _range_check(result):
                    yield result

    return parse


_SNOWFLAKE_REGEX = re.compile(r"<[@&?!#a]{0,3}(?::\w+:)?(\d+)>")
parse_snowflake: _IDMatcherSig = _make_snowflake_parser(_SNOWFLAKE_REGEX)
"""Parse a snowflake from a string or int value.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Other Parameters
----------------
message: str
    The error message to raise if the value cannot be parsed.

Returns
-------
hikari.Snowflake
    The parsed snowflake.

Raises
------
ValueError
    If the value cannot be parsed.
"""

search_snowflakes: _IDSearcherSig = _make_snowflake_searcher(_SNOWFLAKE_REGEX)
"""Iterate over the snowflakes in a string.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Returns
-------
collections.abc.Iterator[hikari.Snowflake]
    An iterator over the snowflakes in the string.
"""

_CHANNEL_ID_REGEX = re.compile(r"<#(\d+)>")
parse_channel_id: _IDMatcherSig = _make_snowflake_parser(_CHANNEL_ID_REGEX)
"""Parse a channel ID from a string or int value.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Other Parameters
----------------
message: str
    The error message to raise if the value cannot be parsed.

Returns
-------
hikari.Snowflake
    The parsed channel ID.

Raises
------
ValueError
    If the value cannot be parsed.
"""

search_channel_ids: _IDSearcherSig = _make_snowflake_searcher(_CHANNEL_ID_REGEX)
"""Iterate over the channel IDs in a string.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Returns
-------
collections.abc.Iterator[hikari.Snowflake]
    An iterator over the channel IDs in the string.
"""

_EMOJI_ID_REGEX = re.compile(r"<a?:\w+:(\d+)>")
parse_emoji_id: _IDMatcherSig = _make_snowflake_parser(_EMOJI_ID_REGEX)
"""Parse an Emoji ID from a string or int value.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Other Parameters
----------------
message: str
    The error message to raise if the value cannot be parsed.

Returns
-------
hikari.Snowflake
    The parsed Emoji ID.

Raises
------
ValueError
    If the value cannot be parsed.
"""

search_emoji_ids: _IDSearcherSig = _make_snowflake_searcher(_EMOJI_ID_REGEX)
"""Iterate over the emoji IDs in a string.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Returns
-------
collections.abc.Iterator[hikari.Snowflake]
    An iterator over the emoji IDs in the string.
"""

_ROLE_ID_REGEX = re.compile(r"<@&(\d+)>")
parse_role_id: _IDMatcherSig = _make_snowflake_parser(_ROLE_ID_REGEX)
"""Parse a role ID from a string or int value.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Other Parameters
----------------
message: str
    The error message to raise if the value cannot be parsed.

Returns
-------
hikari.Snowflake
    The parsed role ID.

Raises
------
ValueError
    If the value cannot be parsed.
"""

search_role_ids: _IDSearcherSig = _make_snowflake_searcher(_ROLE_ID_REGEX)
"""Iterate over the role IDs in a string.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Returns
-------
collections.abc.Iterator[hikari.Snowflake]
    An iterator over the role IDs in the string.
"""

_USER_ID_REGEX = re.compile(r"<@!?(\d+)>")
parse_user_id: _IDMatcherSig = _make_snowflake_parser(_USER_ID_REGEX)
"""Parse a user ID from a string or int value.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Other Parameters
----------------
message: str
    The error message to raise if the value cannot be parsed.

Returns
-------
hikari.Snowflake
    The parsed user ID.

Raises
------
ValueError
    If the value cannot be parsed.
"""

search_user_ids: _IDSearcherSig = _make_snowflake_searcher(_USER_ID_REGEX)
"""Iterate over the user IDs in a string.

Parameters
----------
value: typing.Union[str, int]
    The value to parse (this argument can only be passed positionally).

Returns
-------
collections.abc.Iterator[hikari.Snowflake]
    An iterator over the user IDs in the string.
"""


def _build_url_parser(callback: collections.Callable[[str], _ValueT], /) -> collections.Callable[[str], _ValueT]:
    def parse(value: str, /) -> _ValueT:
        """Convert an argument to a `urllib.parse` type.

        Parameters
        ----------
        value: str
            The value to parse (this argument can only be passed positionally).

        Returns
        -------
        _ValueT
            The parsed URL.

        Raises
        ------
        ValueError
            If the argument couldn't be parsed.
        """
        if value.startswith("<") and value.endswith(">"):
            value = value[1:-1]

        return callback(value)

    return parse


defragment_url: collections.Callable[[str], urlparse.DefragResult] = _build_url_parser(urlparse.urldefrag)
"""Convert an argument to a defragmented URL.

Parameters
----------
value: str
    The value to parse (this argument can only be passed positionally).

Returns
-------
urllib.parse.DefragResult
    The parsed URL.

Raises
------
ValueError
    If the argument couldn't be parsed.
"""

parse_url: collections.Callable[[str], urlparse.ParseResult] = _build_url_parser(urlparse.urlparse)
"""Convert an argument to a parsed URL.

Parameters
----------
value: str
    The value to parse (this argument can only be passed positionally).

Returns
-------
urllib.parse.ParseResult
    The parsed URL.

Raises
------
ValueError
    If the argument couldn't be parsed.
"""


split_url: collections.Callable[[str], urlparse.SplitResult] = _build_url_parser(urlparse.urlsplit)
"""Convert an argument to a split URL.

Parameters
----------
value: str
    The value to parse (this argument can only be passed positionally).

Returns
-------
urllib.parse.SplitResult
    The split URL.

Raises
------
ValueError
    If the argument couldn't be parsed.
"""

_DATETIME_REGEX = re.compile(r"<-?t:(\d+)(?::\w)?>")


def to_datetime(value: str, /) -> datetime.datetime:
    """Parse a datetime from Discord's datetime format.

    More information on this format can be found at
    https://discord.com/developers/docs/reference#message-formatting-timestamp-styles

    Parameters
    ----------
    value: str
        The value to parse.

    Returns
    -------
    datetime.datetime
        The parsed datetime.

    Raises
    ------
    ValueError
        If the value cannot be parsed.
    """
    try:
        timestamp = int(next(_DATETIME_REGEX.finditer(value)).groups()[0])

    except StopIteration:
        raise ValueError("Not a valid datetime") from None

    return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)


_VALID_DATETIME_STYLES = frozenset(("t", "T", "d", "D", "f", "F", "R"))


def from_datetime(value: datetime.datetime, /, *, style: str = "f") -> str:
    """Format a datetime as Discord's datetime format.

    More information on this format can be found at
    https://discord.com/developers/docs/reference#message-formatting-timestamp-styles

    Parameters
    ----------
    value: datetime.datetime
        The datetime to format.

    Other Parameters
    ----------------
    style: str
        The style to use.

        The valid styles can be found at
        https://discord.com/developers/docs/reference#message-formatting-formats
        and this defaults to `"f"`.

    Returns
    -------
    str
        The formatted datetime.

    Raises
    ------
    ValueError
        If the provided datetime is timezone naive.
        If an invalid style is provided.
    """
    if style not in _VALID_DATETIME_STYLES:
        raise ValueError(f"Invalid style: {style}")

    if value.tzinfo is None:
        raise ValueError("Cannot convert naive datetimes, please specify a timezone.")

    return f"<t:{round(value.timestamp())}:{style}>"


_YES_VALUES = frozenset(("y", "yes", "t", "true", "on", "1"))
_NO_VALUES = frozenset(("n", "no", "f", "false", "off", "0"))


def to_bool(value: str, /) -> bool:
    """Convert user string input into a boolean value.

    Parameters
    ----------
    value: str
        The value to convert.

    Returns
    -------
    bool
        The converted value.

    Raises
    ------
    ValueError
        If the value cannot be converted.
    """
    value = value.lower().strip()
    if value in _YES_VALUES:
        return True

    if value in _NO_VALUES:
        return False

    raise ValueError(f"Invalid bool value `{value}`")


def to_color(argument: _ArgumentT, /) -> hikari.Color:
    """Convert user input to a `hikari.colors.Color` object."""
    if isinstance(argument, str):
        values = argument.split(" ")
        if all(value.isdigit() for value in values):
            return hikari.Color.of(*map(int, values))

        return hikari.Color.of(*values)

    return hikari.Color.of(argument)


_TYPE_OVERRIDES: dict[collections.Callable[..., typing.Any], collections.Callable[[str], typing.Any]] = {
    bool: to_bool,
    bytes: lambda d: bytes(d, "utf-8"),
    bytearray: lambda d: bytearray(d, "utf-8"),
    datetime.datetime: to_datetime,
    hikari.Snowflake: parse_snowflake,
    urlparse.DefragResult: defragment_url,
    urlparse.ParseResult: parse_url,
    urlparse.SplitResult: split_url,
}


def override_type(cls: parsing.ConverterSig[typing.Any], /) -> parsing.ConverterSig[typing.Any]:
    return _TYPE_OVERRIDES.get(cls, cls)


to_channel: typing.Final[ToChannel] = ToChannel()
"""Convert user input to a `hikari.channels.PartialChannel` object."""

to_colour: typing.Final[collections.Callable[[_ArgumentT], hikari.Color]] = to_color
"""Convert user input to a `hikari.colors.Color` object."""

to_emoji: typing.Final[ToEmoji] = ToEmoji()
"""Convert user input to a cached `hikari.emojis.KnownCustomEmoji` object.

.. note::
    If you just want to convert inpute to a `hikari.Emoji`, `hikari.CustomEmoji`
    or `hikari.UnicodeEmoji` without making any cache or REST calls then you
    can just use the relevant `hikari.Emoji.parse`, `hikari.CustomEmoji.parse`
    or `hikari.UnicodeEmoji.parse` methods.
"""

to_guild: typing.Final[ToGuild] = ToGuild()
"""Convert user input to a `hikari.guilds.Guild` object."""

to_invite: typing.Final[ToInvite] = ToInvite()
"""Convert user input to a cached `hikari.invites.InviteWithMetadata` object."""

to_invite_with_metadata: typing.Final[ToInviteWithMetadata] = ToInviteWithMetadata()
"""Convert user input to a `hikari.invites.Invite` object."""

to_member: typing.Final[ToMember] = ToMember()
"""Convert user input to a `hikari.guilds.Member` object."""

to_presence: typing.Final[ToPresence] = ToPresence()
"""Convert user input to a cached `hikari.presences.MemberPresence`."""

to_role: typing.Final[ToRole] = ToRole()
"""Convert user input to a `hikari.guilds.Role` object."""

to_snowflake: typing.Final[collections.Callable[[_ArgumentT], hikari.Snowflake]] = parse_snowflake
"""Convert user input to a `hikari.snowflakes.Snowflake`.

.. note::
    This also range validates the input.
"""

to_user: typing.Final[ToUser] = ToUser()
"""Convert user input to a `hikari.users.User` object."""

to_voice_state: typing.Final[ToVoiceState] = ToVoiceState()
"""Convert user input to a cached `hikari.voices.VoiceState`."""

Functions and classes used to enable more Discord oriented argument converters.

#   def to_bool(value: str, /) -> bool:
View Source
def to_bool(value: str, /) -> bool:
    """Convert user string input into a boolean value.

    Parameters
    ----------
    value: str
        The value to convert.

    Returns
    -------
    bool
        The converted value.

    Raises
    ------
    ValueError
        If the value cannot be converted.
    """
    value = value.lower().strip()
    if value in _YES_VALUES:
        return True

    if value in _NO_VALUES:
        return False

    raise ValueError(f"Invalid bool value `{value}`")

Convert user string input into a boolean value.

Parameters
  • value (str): The value to convert.
Returns
  • bool: The converted value.
Raises
  • ValueError: If the value cannot be converted.
#   to_channel = <tanjun.conversion.ToChannel object>
#   def to_color(argument: Union[str, int, float], /) -> hikari.colors.Color:
View Source
def to_color(argument: _ArgumentT, /) -> hikari.Color:
    """Convert user input to a `hikari.colors.Color` object."""
    if isinstance(argument, str):
        values = argument.split(" ")
        if all(value.isdigit() for value in values):
            return hikari.Color.of(*map(int, values))

        return hikari.Color.of(*values)

    return hikari.Color.of(argument)

Convert user input to a hikari.colors.Color object.

#   def to_colour(argument: Union[str, int, float], /) -> hikari.colors.Color:
View Source
def to_color(argument: _ArgumentT, /) -> hikari.Color:
    """Convert user input to a `hikari.colors.Color` object."""
    if isinstance(argument, str):
        values = argument.split(" ")
        if all(value.isdigit() for value in values):
            return hikari.Color.of(*map(int, values))

        return hikari.Color.of(*values)

    return hikari.Color.of(argument)

Convert user input to a hikari.colors.Color object.

#   def to_datetime(value: str, /) -> datetime.datetime:
View Source
def to_datetime(value: str, /) -> datetime.datetime:
    """Parse a datetime from Discord's datetime format.

    More information on this format can be found at
    https://discord.com/developers/docs/reference#message-formatting-timestamp-styles

    Parameters
    ----------
    value: str
        The value to parse.

    Returns
    -------
    datetime.datetime
        The parsed datetime.

    Raises
    ------
    ValueError
        If the value cannot be parsed.
    """
    try:
        timestamp = int(next(_DATETIME_REGEX.finditer(value)).groups()[0])

    except StopIteration:
        raise ValueError("Not a valid datetime") from None

    return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)

Parse a datetime from Discord's datetime format.

More information on this format can be found at https://discord.com/developers/docs/reference#message-formatting-timestamp-styles

Parameters
  • value (str): The value to parse.
Returns
  • datetime.datetime: The parsed datetime.
Raises
  • ValueError: If the value cannot be parsed.
#   to_emoji = <tanjun.conversion.ToEmoji object>
#   to_guild = <tanjun.conversion.ToGuild object>
#   to_invite = <tanjun.conversion.ToInvite object>
#   to_invite_with_metadata = <tanjun.conversion.ToInviteWithMetadata object>
#   to_member = <tanjun.conversion.ToMember object>
#   to_presence = <tanjun.conversion.ToPresence object>
#   to_role = <tanjun.conversion.ToRole object>
#   def to_snowflake( value: Union[str, int, float], /, *, message: str = 'No valid mention or ID found' ) -> hikari.snowflakes.Snowflake:
View Source
    def parse(value: _ArgumentT, /, *, message: str = "No valid mention or ID found") -> hikari.Snowflake:
        """Parse a snowflake from a string or int value.

        .. note::
            This only allows the relevant entity's mention format if applicable.

        Parameters
        ----------
        value: typing.Union[str, int]
            The value to parse (this argument can only be passed positionally).

        Other Parameters
        ----------------
        message: str
            The error message to raise if the value cannot be parsed.

        Returns
        -------
        hikari.Snowflake
            The parsed snowflake.

        Raises
        ------
        ValueError
            If the value cannot be parsed.
        """
        result: typing.Optional[hikari.Snowflake] = None
        if isinstance(value, str):
            if value.isdigit():
                result = hikari.Snowflake(value)

            else:
                capture = next(regex.finditer(value), None)
                result = hikari.Snowflake(capture.groups()[0]) if capture else None

        else:
            try:
                # Technically passing a float here is invalid (typing wise)
                # but we handle that by catching TypeError
                result = hikari.Snowflake(operator.index(typing.cast(int, value)))

            except (TypeError, ValueError):
                pass

        # We should also range check the provided ID.
        if result is not None and _range_check(result):
            return result

        raise ValueError(message) from None

Parse a snowflake from a string or int value.

Note: This only allows the relevant entity's mention format if applicable.

Parameters
  • value (typing.Union[str, int]): The value to parse (this argument can only be passed positionally).
Other Parameters
  • message (str): The error message to raise if the value cannot be parsed.
Returns
  • hikari.Snowflake: The parsed snowflake.
Raises
  • ValueError: If the value cannot be parsed.
#   to_user = <tanjun.conversion.ToUser object>
#   to_voice_state = <tanjun.conversion.ToVoiceState object>
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Default dependency utilities used within Tanjun and their abstract interfaces."""
from __future__ import annotations

__all__: list[str] = [
    # __init__.py
    "set_standard_dependencies",
    # async_cache.py
    "async_cache",
    "AsyncCache",
    "ChannelBoundCache",
    "CacheIterator",
    "CacheMissError",
    "EntryNotFound",
    "GuildBoundCache",
    "SingleStoreCache",
    "SfCache",
    "SfChannelBound",
    "SfGuildBound",
    # callbacks.py
    "callbacks",
    "fetch_my_user",
    # data.py
    "data",
    "cache_callback",
    "cached_inject",
    "LazyConstant",
    "inject_lc",
    "make_lc_resolver",
    # limiters.py
    "limiters",
    "AbstractConcurrencyLimiter",
    "AbstractCooldownManager",
    "BucketResource",
    "ConcurrencyPreExecution",
    "ConcurrencyPostExecution",
    "CooldownPreExecution",
    "InMemoryConcurrencyLimiter",
    "InMemoryCooldownManager",
    "with_concurrency_limit",
    "with_cooldown",
    # owners.py
    "owners",
    "AbstractOwners",
    "Owners",
]

import hikari

from .. import injecting
from .async_cache import AsyncCache
from .async_cache import CacheIterator
from .async_cache import CacheMissError
from .async_cache import ChannelBoundCache
from .async_cache import EntryNotFound
from .async_cache import GuildBoundCache
from .async_cache import SfCache
from .async_cache import SfChannelBound
from .async_cache import SfGuildBound
from .async_cache import SingleStoreCache
from .callbacks import fetch_my_user
from .data import LazyConstant
from .data import cache_callback
from .data import cached_inject
from .data import inject_lc
from .data import make_lc_resolver
from .limiters import AbstractConcurrencyLimiter
from .limiters import AbstractCooldownManager
from .limiters import BucketResource
from .limiters import ConcurrencyPostExecution
from .limiters import ConcurrencyPreExecution
from .limiters import CooldownPreExecution
from .limiters import InMemoryConcurrencyLimiter
from .limiters import InMemoryCooldownManager
from .limiters import with_concurrency_limit
from .limiters import with_cooldown
from .owners import AbstractOwners
from .owners import Owners


def set_standard_dependencies(client: injecting.InjectorClient, /) -> None:
    """Set the standard dependencies for Tanjun.

    Parameters
    ----------
    client: tanjun.injecting.InjectorClient
        The injector client to set the standard dependencies on.
    """
    client.set_type_dependency(AbstractOwners, Owners()).set_type_dependency(
        LazyConstant[hikari.OwnUser], LazyConstant[hikari.OwnUser](fetch_my_user)
    )

Default dependency utilities used within Tanjun and their abstract interfaces.

#   class BucketResource(builtins.int, enum.Enum):
View Source
class BucketResource(int, enum.Enum):
    """Resource target types used within command calldowns and concurrency limiters."""

    USER = 0
    """A per-user resource bucket."""

    MEMBER = 1
    """A per-guild member resource bucket.

    .. note::
        When executed in a DM this will be per-DM.
    """

    CHANNEL = 2
    """A per-channel resource bucket."""

    PARENT_CHANNEL = 3
    """A per-parent channel resource bucket.

    .. note::
        For DM channels this will be per-DM, for guild channels with no parents
        this'll be per-guild.
    """

    # CATEGORY = 4
    # """A per-category resource bucket.

    # .. note::
    #     For DM channels this will be per-DM, for guild channels with no parent
    #     category this'll be per-guild.
    # """

    TOP_ROLE = 5
    """A per-highest role resource bucket.

    .. note::
        When executed in a DM this will be per-DM, with this defaulting to
        targeting the @everyone role if they have no real roles.
    """

    GUILD = 6
    """A per-guild resource bucket.

    .. note::
        When executed in a DM this will be per-DM.
    """

    GLOBAL = 7
    """A global resource bucket."""

Resource target types used within command calldowns and concurrency limiters.

A per-user resource bucket.

A per-guild member resource bucket.

Note: When executed in a DM this will be per-DM.

A per-channel resource bucket.

#   PARENT_CHANNEL = <BucketResource.PARENT_CHANNEL: 3>

A per-parent channel resource bucket.

Note: For DM channels this will be per-DM, for guild channels with no parents this'll be per-guild.

A per-highest role resource bucket.

Note: When executed in a DM this will be per-DM, with this defaulting to targeting the @everyone role if they have no real roles.

A per-guild resource bucket.

Note: When executed in a DM this will be per-DM.

A global resource bucket.

Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
to_bytes
from_bytes
as_integer_ratio
real
imag
numerator
denominator
#   def cached_inject( callback: collections.abc.Callable[..., typing.Union[~_T, collections.abc.Awaitable[~_T]]], /, *, expire_after: Union[float, int, datetime.timedelta, NoneType] = None ) -> ~_T:
View Source
def cached_inject(
    callback: injecting.CallbackSig[_T], /, *, expire_after: typing.Union[float, int, datetime.timedelta, None] = None
) -> _T:
    """Inject a callback with caching.

    This acts like `tanjun.injecting.inject` and the result of it
    should also be assigned to a parameter's default to be used.

    Example
    -------
    ```py
    async def resolve_database(
        client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client)
    ) -> Database:
        raise NotImplementedError

    @tanjun.as_message_command("command name")
    async def command(
        ctx: tanjun.abc.Context, db: Database = tanjun.cached_inject(resolve_database)
    ) -> None:
        raise NotImplementedError

    Parameters
    ----------
    callback : CallbackSig[_T]
        The callback to inject.

    Other Parameters
    ----------------
    expire_after : typing.Union[int, float, datetime.timedelta, None]
        The amount of time to cache the result for in seconds.

        Leave this as `None` to cache for the runtime of the application.

    Returns
    -------
    tanjun.injecting.Injected[_T]
        Injector used to resolve the cached callback.

    Raises
    ------
    ValueError
        If expire_after is not a valid value.
        If expire_after is not less than or equal to 0 seconds.
    """
    return injecting.inject(callback=cache_callback(callback, expire_after=expire_after))

Inject a callback with caching.

This acts like tanjun.injecting.inject and the result of it should also be assigned to a parameter's default to be used.

Example

```py async def resolve_database( client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client) ) -> Database: raise NotImplementedError

@tanjun.as_message_command("command name") async def command( ctx: tanjun.abc.Context, db: Database = tanjun.cached_inject(resolve_database) ) -> None: raise NotImplementedError

Parameters
  • callback (CallbackSig[_T]): The callback to inject.
Other Parameters
  • expire_after (typing.Union[int, float, datetime.timedelta, None]): The amount of time to cache the result for in seconds.

    Leave this as None to cache for the runtime of the application.

Returns
Raises
  • ValueError: If expire_after is not a valid value. If expire_after is not less than or equal to 0 seconds.
#   def inject_lc(type_: type[~_T], /) -> ~_T:
View Source
def inject_lc(type_: type[_T], /) -> _T:
    """Make a LazyConstant injector.

    This acts like `tanjun.injecting.inject` and the result of it
    should also be assigned to a parameter's default to be used.

    .. note::
        For this to work, a `LazyConstant` must've been set as a type
        dependency for the passed `type_`.

    Parameters
    ----------
    type_ : type[_T]
        The type of the constant to resolve.

    Returns
    -------
    tanjun.injecting.Injected[_T]
        Injector used to resolve the LazyConstant.

    Example
    -------
    ```py
    @component.with_command
    @tanjun.as_message_command
    async def command(
        ctx: tanjun.abc.MessageCommand,
        application: hikari.Application = tanjun.inject_lc(hikari.Application)
    ) -> None:
        raise NotImplementedError

    ...

    async def resolve_app(
        client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client)
    ) -> hikari.Application:
        raise NotImplementedError

    tanjun.Client.from_gateway_bot(...).set_type_dependency(
        tanjun.LazyConstant[hikari.Application] = tanjun.LazyConstant(resolve_app)
    )
    ```
    """
    return injecting.inject(callback=make_lc_resolver(type_))

Make a LazyConstant injector.

This acts like tanjun.injecting.inject and the result of it should also be assigned to a parameter's default to be used.

Note: For this to work, a LazyConstant must've been set as a type dependency for the passed type_.

Parameters
  • type_ (type[_T]): The type of the constant to resolve.
Returns
Example
@component.with_command
@tanjun.as_message_command
async def command(
    ctx: tanjun.abc.MessageCommand,
    application: hikari.Application = tanjun.inject_lc(hikari.Application)
) -> None:
    raise NotImplementedError

...

async def resolve_app(
    client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client)
) -> hikari.Application:
    raise NotImplementedError

tanjun.Client.from_gateway_bot(...).set_type_dependency(
    tanjun.LazyConstant[hikari.Application] = tanjun.LazyConstant(resolve_app)
)
View Source
class InMemoryConcurrencyLimiter(AbstractConcurrencyLimiter):
    """In-memory standard implementation of `AbstractConcurrencyLimiter`.

    Examples
    --------
    `InMemoryConcurrencyLimiter.set_bucket` may be used to set the concurrency
    limits for a specific bucket:

    ```py
    (
        InMemoryConcurrencyLimiter()
        # Set the default bucket template to 10 concurrent uses of the command per-user.
        .set_bucket("default", tanjun.BucketResource.USER, 10)
        # Set the "moderation" bucket with a limit of 5 concurrent uses per-guild.
        .set_bucket("moderation", tanjun.BucketResource.GUILD, 5)
        .set_bucket()
        # add_to_client will setup the concurrency manager (setting it as an
        # injected dependency and registering callbacks to manage it).
        .add_to_client(client)
    )
    ```
    """

    __slots__ = ("_acquiring_ctxs", "_buckets", "_default_bucket_template", "_gc_task")

    def __init__(self) -> None:
        self._acquiring_ctxs: dict[tuple[str, tanjun_abc.Context], _ConcurrencyLimit] = {}
        self._buckets: dict[str, _BaseResource[_ConcurrencyLimit]] = {}
        self._default_bucket_template: _BaseResource[_ConcurrencyLimit] = _FlatResource(
            BucketResource.USER, lambda: _ConcurrencyLimit(limit=1)
        )
        self._gc_task: typing.Optional[asyncio.Task[None]] = None

    async def _gc(self) -> None:
        while True:
            await asyncio.sleep(10)
            for bucket in self._buckets.values():
                bucket.cleanup()

    def add_to_client(self, client: injecting.InjectorClient, /) -> None:
        """Add this concurrency manager to a tanjun client.

        .. note::
            This registers the manager as a type dependency and manages opening
            and closing the manager based on the client's life cycle.

        Parameters
        ----------
        client : tanjun.abc.Client
            The client to add this concurrency manager to.
        """
        client.set_type_dependency(AbstractConcurrencyLimiter, self)
        # TODO: the injection client should be upgraded to the abstract Client.
        assert isinstance(client, tanjun_abc.Client)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close)
        if client.is_alive:
            assert client.loop is not None
            self.open(_loop=client.loop)

    def close(self) -> None:
        """Stop the concurrency manager.

        Raises
        ------
        RuntimeError
            If the concurrency manager is not running.
        """
        if not self._gc_task:
            raise RuntimeError("Concurrency manager is not active")

        self._gc_task.cancel()
        self._gc_task = None

    def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None:
        """Start the concurrency manager.

        Raises
        ------
        RuntimeError
            If the concurrency manager is already running.
            If called in a thread with no running event loop.
        """
        if self._gc_task:
            raise RuntimeError("Concurrency manager is already running")

        self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc())

    async def try_acquire(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> bool:
        # <<inherited docstring from AbstractConcurrencyLimiter>>.
        bucket = self._buckets.get(bucket_id)
        if not bucket:
            _LOGGER.info("No concurrency limit found for %r, falling back to 'default' bucket", bucket_id)
            bucket = self._buckets[bucket_id] = self._default_bucket_template.copy()

        # incrementing a bucket multiple times for the same context could lead
        # to weird edge cases based on how we internally track this, so we
        # internally de-duplicate this.
        elif (bucket_id, ctx) in self._acquiring_ctxs:
            return True  # This won't ever be the case if it just had to make a new bucket, hence the elif.

        if result := (limit := await bucket.into_inner(ctx)).acquire():
            self._acquiring_ctxs[(bucket_id, ctx)] = limit

        return result

    async def release(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None:
        # <<inherited docstring from AbstractConcurrencyLimiter>>.
        if limit := self._acquiring_ctxs.pop((bucket_id, ctx), None):
            limit.release()

    def disable_bucket(self: _InMemoryConcurrencyLimiterT, bucket_id: str, /) -> _InMemoryConcurrencyLimiterT:
        """Disable a concurrency limit bucket.

        This will stop the bucket from ever hitting a concurrency limit
        and also prevents the bucket from defaulting.

        Parameters
        ----------
        bucket_id : str
            The bucket to disable.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.

        Returns
        -------
        Self
            This concurrency manager to allow for chaining.
        """
        bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _ConcurrencyLimit(limit=-1))
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

    def set_bucket(
        self: _InMemoryConcurrencyLimiterT, bucket_id: str, resource: BucketResource, limit: int, /
    ) -> _InMemoryConcurrencyLimiterT:
        """Set the concurrency limit for a specific bucket.

        Parameters
        ----------
        bucket_id : str
            The ID of the bucket to set the concurrency limit for.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.
        resource : tanjun.BucketResource
            The type of resource to target for the concurrency limit.
        limit : int
            The maximum number of concurrent uses to allow.

        Returns
        -------
        Self
            The concurrency manager to allow call chaining.

        Raises
        ------
        ValueError
            If an invalid resource type is given.
            if limit is less 0 or negative.
        """
        if limit <= 0:
            raise ValueError("limit must be greater than 0")

        bucket = self._buckets[bucket_id] = _to_bucket(BucketResource(resource), lambda: _ConcurrencyLimit(limit=limit))
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

In-memory standard implementation of AbstractConcurrencyLimiter.

Examples

InMemoryConcurrencyLimiter.set_bucket may be used to set the concurrency limits for a specific bucket:

(
    InMemoryConcurrencyLimiter()
    # Set the default bucket template to 10 concurrent uses of the command per-user.
    .set_bucket("default", tanjun.BucketResource.USER, 10)
    # Set the "moderation" bucket with a limit of 5 concurrent uses per-guild.
    .set_bucket("moderation", tanjun.BucketResource.GUILD, 5)
    .set_bucket()
    # add_to_client will setup the concurrency manager (setting it as an
    # injected dependency and registering callbacks to manage it).
    .add_to_client(client)
)
#   InMemoryConcurrencyLimiter()
View Source
    def __init__(self) -> None:
        self._acquiring_ctxs: dict[tuple[str, tanjun_abc.Context], _ConcurrencyLimit] = {}
        self._buckets: dict[str, _BaseResource[_ConcurrencyLimit]] = {}
        self._default_bucket_template: _BaseResource[_ConcurrencyLimit] = _FlatResource(
            BucketResource.USER, lambda: _ConcurrencyLimit(limit=1)
        )
        self._gc_task: typing.Optional[asyncio.Task[None]] = None
#   def add_to_client(self, client: tanjun.injecting.InjectorClient, /) -> None:
View Source
    def add_to_client(self, client: injecting.InjectorClient, /) -> None:
        """Add this concurrency manager to a tanjun client.

        .. note::
            This registers the manager as a type dependency and manages opening
            and closing the manager based on the client's life cycle.

        Parameters
        ----------
        client : tanjun.abc.Client
            The client to add this concurrency manager to.
        """
        client.set_type_dependency(AbstractConcurrencyLimiter, self)
        # TODO: the injection client should be upgraded to the abstract Client.
        assert isinstance(client, tanjun_abc.Client)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close)
        if client.is_alive:
            assert client.loop is not None
            self.open(_loop=client.loop)

Add this concurrency manager to a tanjun client.

Note: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle.

Parameters
#   def close(self) -> None:
View Source
    def close(self) -> None:
        """Stop the concurrency manager.

        Raises
        ------
        RuntimeError
            If the concurrency manager is not running.
        """
        if not self._gc_task:
            raise RuntimeError("Concurrency manager is not active")

        self._gc_task.cancel()
        self._gc_task = None

Stop the concurrency manager.

Raises
  • RuntimeError: If the concurrency manager is not running.
#   def open( self, *, _loop: Optional[asyncio.events.AbstractEventLoop] = None ) -> None:
View Source
    def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None:
        """Start the concurrency manager.

        Raises
        ------
        RuntimeError
            If the concurrency manager is already running.
            If called in a thread with no running event loop.
        """
        if self._gc_task:
            raise RuntimeError("Concurrency manager is already running")

        self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc())

Start the concurrency manager.

Raises
  • RuntimeError: If the concurrency manager is already running. If called in a thread with no running event loop.
#   async def try_acquire(self, bucket_id: str, ctx: tanjun.abc.Context, /) -> bool:
View Source
    async def try_acquire(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> bool:
        # <<inherited docstring from AbstractConcurrencyLimiter>>.
        bucket = self._buckets.get(bucket_id)
        if not bucket:
            _LOGGER.info("No concurrency limit found for %r, falling back to 'default' bucket", bucket_id)
            bucket = self._buckets[bucket_id] = self._default_bucket_template.copy()

        # incrementing a bucket multiple times for the same context could lead
        # to weird edge cases based on how we internally track this, so we
        # internally de-duplicate this.
        elif (bucket_id, ctx) in self._acquiring_ctxs:
            return True  # This won't ever be the case if it just had to make a new bucket, hence the elif.

        if result := (limit := await bucket.into_inner(ctx)).acquire():
            self._acquiring_ctxs[(bucket_id, ctx)] = limit

        return result

Try to acquire a concurrency lock on a bucket.

Parameters
  • bucket_id (str): The concurrency bucket to acquire.
  • ctx (tanjun.abc.Context): The context to acquire this resource lock with.
Returns
  • bool: Whether the lock was acquired.
#   async def release(self, bucket_id: str, ctx: tanjun.abc.Context, /) -> None:
View Source
    async def release(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None:
        # <<inherited docstring from AbstractConcurrencyLimiter>>.
        if limit := self._acquiring_ctxs.pop((bucket_id, ctx), None):
            limit.release()

Release a concurrency lock on a bucket.

#   def disable_bucket( self: ~_InMemoryConcurrencyLimiterT, bucket_id: str, / ) -> ~_InMemoryConcurrencyLimiterT:
View Source
    def disable_bucket(self: _InMemoryConcurrencyLimiterT, bucket_id: str, /) -> _InMemoryConcurrencyLimiterT:
        """Disable a concurrency limit bucket.

        This will stop the bucket from ever hitting a concurrency limit
        and also prevents the bucket from defaulting.

        Parameters
        ----------
        bucket_id : str
            The bucket to disable.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.

        Returns
        -------
        Self
            This concurrency manager to allow for chaining.
        """
        bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _ConcurrencyLimit(limit=-1))
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

Disable a concurrency limit bucket.

This will stop the bucket from ever hitting a concurrency limit and also prevents the bucket from defaulting.

Parameters
  • bucket_id (str): The bucket to disable.

    Note: "default" is a special bucket which is used as a template for unknown bucket IDs.

Returns
  • Self: This concurrency manager to allow for chaining.
#   def set_bucket( self: ~_InMemoryConcurrencyLimiterT, bucket_id: str, resource: tanjun.dependencies.limiters.BucketResource, limit: int, / ) -> ~_InMemoryConcurrencyLimiterT:
View Source
    def set_bucket(
        self: _InMemoryConcurrencyLimiterT, bucket_id: str, resource: BucketResource, limit: int, /
    ) -> _InMemoryConcurrencyLimiterT:
        """Set the concurrency limit for a specific bucket.

        Parameters
        ----------
        bucket_id : str
            The ID of the bucket to set the concurrency limit for.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.
        resource : tanjun.BucketResource
            The type of resource to target for the concurrency limit.
        limit : int
            The maximum number of concurrent uses to allow.

        Returns
        -------
        Self
            The concurrency manager to allow call chaining.

        Raises
        ------
        ValueError
            If an invalid resource type is given.
            if limit is less 0 or negative.
        """
        if limit <= 0:
            raise ValueError("limit must be greater than 0")

        bucket = self._buckets[bucket_id] = _to_bucket(BucketResource(resource), lambda: _ConcurrencyLimit(limit=limit))
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

Set the concurrency limit for a specific bucket.

Parameters
  • bucket_id (str): The ID of the bucket to set the concurrency limit for.

    Note: "default" is a special bucket which is used as a template for unknown bucket IDs.

  • resource (tanjun.BucketResource): The type of resource to target for the concurrency limit.
  • limit (int): The maximum number of concurrent uses to allow.
Returns
  • Self: The concurrency manager to allow call chaining.
Raises
  • ValueError: If an invalid resource type is given. if limit is less 0 or negative.
View Source
class InMemoryCooldownManager(AbstractCooldownManager):
    """In-memory standard implementation of `AbstractCooldownManager`.

    Examples
    --------
    `InMemoryCooldownManager.set_bucket` may be used to set the cooldown for a
    specific bucket:

    ```py
    (
        InMemoryCooldownManager()
        # Set the default bucket template to a per-user 10 uses per-60 seconds cooldown.
        .set_bucket("default", tanjun.BucketResource.USER, 10, 60)
        # Set the "moderation" bucket to a per-guild 100 uses per-5 minutes cooldown.
        .set_bucket("moderation", tanjun.BucketResource.GUILD, 100, datetime.timedelta(minutes=5))
        .set_bucket()
        # add_to_client will setup the cooldown manager (setting it as an
        # injected dependency and registering callbacks to manage it).
        .add_to_client(client)
    )
    ```
    """

    __slots__ = ("_buckets", "_default_bucket_template", "_gc_task")

    def __init__(self) -> None:
        self._buckets: dict[str, _BaseResource[_Cooldown]] = {}
        self._default_bucket_template: _BaseResource[_Cooldown] = _FlatResource(
            BucketResource.USER, lambda: _Cooldown(limit=2, reset_after=5)
        )
        self._gc_task: typing.Optional[asyncio.Task[None]] = None

    def _get_or_default(self, bucket_id: str, /) -> _BaseResource[_Cooldown]:
        if bucket := self._buckets.get(bucket_id):
            return bucket

        _LOGGER.info("No cooldown found for %r, falling back to 'default' bucket", bucket_id)
        bucket = self._buckets[bucket_id] = self._default_bucket_template.copy()
        return bucket

    async def _gc(self) -> None:
        while True:
            await asyncio.sleep(10)
            for bucket in self._buckets.values():
                bucket.cleanup()

    def add_to_client(self, client: injecting.InjectorClient, /) -> None:
        """Add this cooldown manager to a tanjun client.

        .. note::
            This registers the manager as a type dependency and manages opening
            and closing the manager based on the client's life cycle.

        Parameters
        ----------
        client : tanjun.abc.Client
            The client to add this cooldown manager to.
        """
        client.set_type_dependency(AbstractCooldownManager, self)
        # TODO: the injection client should be upgraded to the abstract Client.
        assert isinstance(client, tanjun_abc.Client)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close)
        if client.is_alive:
            assert client.loop is not None
            self.open(_loop=client.loop)

    async def check_cooldown(
        self, bucket_id: str, ctx: tanjun_abc.Context, /, *, increment: bool = False
    ) -> typing.Optional[float]:
        # <<inherited docstring from AbstractCooldownManager>>.
        if increment:
            bucket = await self._get_or_default(bucket_id).into_inner(ctx)
            if cooldown := bucket.must_wait_for():
                return cooldown

            bucket.increment()
            return None

        if (bucket := self._buckets.get(bucket_id)) and (cooldown := await bucket.try_into_inner(ctx)):
            return cooldown.must_wait_for()

    async def increment_cooldown(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None:
        # <<inherited docstring from AbstractCooldownManager>>.
        (await self._get_or_default(bucket_id).into_inner(ctx)).increment()

    def close(self) -> None:
        """Stop the cooldown manager.

        Raises
        ------
        RuntimeError
            If the cooldown manager is not running.
        """
        if not self._gc_task:
            raise RuntimeError("Cooldown manager is not active")

        self._gc_task.cancel()
        self._gc_task = None

    def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None:
        """Start the cooldown manager.

        Raises
        ------
        RuntimeError
            If the cooldown manager is already running.
            If called in a thread with no running event loop.
        """
        if self._gc_task:
            raise RuntimeError("Cooldown manager is already running")

        self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc())

    def disable_bucket(self: _InMemoryCooldownManagerT, bucket_id: str, /) -> _InMemoryCooldownManagerT:
        """Disable a cooldown bucket.

        This will stop the bucket from ever hitting a cooldown and also
        prevents the bucket from defaulting.

        Parameters
        ----------
        bucket_id : str
            The bucket to disable.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.

        Returns
        -------
        Self
            This cooldown manager to allow for chaining.
        """
        # A limit of -1 is special cased to mean no limit and reset_after is ignored in this scenario.
        bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _Cooldown(limit=-1, reset_after=-1))
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

    def set_bucket(
        self: _InMemoryCooldownManagerT,
        bucket_id: str,
        resource: BucketResource,
        limit: int,
        reset_after: typing.Union[int, float, datetime.timedelta],
        /,
    ) -> _InMemoryCooldownManagerT:
        """Set the cooldown for a specific bucket.

        Parameters
        ----------
        bucket_id : str
            The ID of the bucket to set the cooldown for.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.
        resource : tanjun.BucketResource
            The type of resource to target for the cooldown.
        limit : int
            The number of uses per cooldown period.
        reset_after : int, float, datetime.timedelta
            The cooldown period.

        Returns
        -------
        Self
            The cooldown manager to allow call chaining.

        Raises
        ------
        ValueError
            If an invalid resource type is given.
            If reset_after or limit are negative, 0 or invalid.
            if limit is less 0 or negative.
        """
        if isinstance(reset_after, datetime.timedelta):
            reset_after_seconds = reset_after.total_seconds()
        else:
            reset_after_seconds = float(reset_after)

        if reset_after_seconds <= 0:
            raise ValueError("reset_after must be greater than 0 seconds")

        if limit <= 0:
            raise ValueError("limit must be greater than 0")

        bucket = self._buckets[bucket_id] = _to_bucket(
            BucketResource(resource), lambda: _Cooldown(limit=limit, reset_after=reset_after_seconds)
        )
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

In-memory standard implementation of AbstractCooldownManager.

Examples

InMemoryCooldownManager.set_bucket may be used to set the cooldown for a specific bucket:

(
    InMemoryCooldownManager()
    # Set the default bucket template to a per-user 10 uses per-60 seconds cooldown.
    .set_bucket("default", tanjun.BucketResource.USER, 10, 60)
    # Set the "moderation" bucket to a per-guild 100 uses per-5 minutes cooldown.
    .set_bucket("moderation", tanjun.BucketResource.GUILD, 100, datetime.timedelta(minutes=5))
    .set_bucket()
    # add_to_client will setup the cooldown manager (setting it as an
    # injected dependency and registering callbacks to manage it).
    .add_to_client(client)
)
#   InMemoryCooldownManager()
View Source
    def __init__(self) -> None:
        self._buckets: dict[str, _BaseResource[_Cooldown]] = {}
        self._default_bucket_template: _BaseResource[_Cooldown] = _FlatResource(
            BucketResource.USER, lambda: _Cooldown(limit=2, reset_after=5)
        )
        self._gc_task: typing.Optional[asyncio.Task[None]] = None
#   def add_to_client(self, client: tanjun.injecting.InjectorClient, /) -> None:
View Source
    def add_to_client(self, client: injecting.InjectorClient, /) -> None:
        """Add this cooldown manager to a tanjun client.

        .. note::
            This registers the manager as a type dependency and manages opening
            and closing the manager based on the client's life cycle.

        Parameters
        ----------
        client : tanjun.abc.Client
            The client to add this cooldown manager to.
        """
        client.set_type_dependency(AbstractCooldownManager, self)
        # TODO: the injection client should be upgraded to the abstract Client.
        assert isinstance(client, tanjun_abc.Client)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open)
        client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close)
        if client.is_alive:
            assert client.loop is not None
            self.open(_loop=client.loop)

Add this cooldown manager to a tanjun client.

Note: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle.

Parameters
#   async def check_cooldown( self, bucket_id: str, ctx: tanjun.abc.Context, /, *, increment: bool = False ) -> Optional[float]:
View Source
    async def check_cooldown(
        self, bucket_id: str, ctx: tanjun_abc.Context, /, *, increment: bool = False
    ) -> typing.Optional[float]:
        # <<inherited docstring from AbstractCooldownManager>>.
        if increment:
            bucket = await self._get_or_default(bucket_id).into_inner(ctx)
            if cooldown := bucket.must_wait_for():
                return cooldown

            bucket.increment()
            return None

        if (bucket := self._buckets.get(bucket_id)) and (cooldown := await bucket.try_into_inner(ctx)):
            return cooldown.must_wait_for()

Check if a bucket is on cooldown for the provided context.

Parameters
  • bucket_id (str): The cooldown bucket to check.
  • ctx (tanjun.abc.Context): The context of the command.
Other Parameters
  • increment (bool): Whether this call should increment the bucket's use counter if it isn't depleted.
Returns
  • typing.Optional[float]: When this command will next be usable for the provided context if it's in cooldown else None.
#   async def increment_cooldown(self, bucket_id: str, ctx: tanjun.abc.Context, /) -> None:
View Source
    async def increment_cooldown(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None:
        # <<inherited docstring from AbstractCooldownManager>>.
        (await self._get_or_default(bucket_id).into_inner(ctx)).increment()

Increment the cooldown of a cooldown bucket.

Parameters
  • bucket_id (str): The cooldown bucket's ID.
  • ctx (tanjun.abc.Context): The context of the command.
#   def close(self) -> None:
View Source
    def close(self) -> None:
        """Stop the cooldown manager.

        Raises
        ------
        RuntimeError
            If the cooldown manager is not running.
        """
        if not self._gc_task:
            raise RuntimeError("Cooldown manager is not active")

        self._gc_task.cancel()
        self._gc_task = None

Stop the cooldown manager.

Raises
  • RuntimeError: If the cooldown manager is not running.
#   def open( self, *, _loop: Optional[asyncio.events.AbstractEventLoop] = None ) -> None:
View Source
    def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None:
        """Start the cooldown manager.

        Raises
        ------
        RuntimeError
            If the cooldown manager is already running.
            If called in a thread with no running event loop.
        """
        if self._gc_task:
            raise RuntimeError("Cooldown manager is already running")

        self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc())

Start the cooldown manager.

Raises
  • RuntimeError: If the cooldown manager is already running. If called in a thread with no running event loop.
#   def disable_bucket( self: ~_InMemoryCooldownManagerT, bucket_id: str, / ) -> ~_InMemoryCooldownManagerT:
View Source
    def disable_bucket(self: _InMemoryCooldownManagerT, bucket_id: str, /) -> _InMemoryCooldownManagerT:
        """Disable a cooldown bucket.

        This will stop the bucket from ever hitting a cooldown and also
        prevents the bucket from defaulting.

        Parameters
        ----------
        bucket_id : str
            The bucket to disable.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.

        Returns
        -------
        Self
            This cooldown manager to allow for chaining.
        """
        # A limit of -1 is special cased to mean no limit and reset_after is ignored in this scenario.
        bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _Cooldown(limit=-1, reset_after=-1))
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

Disable a cooldown bucket.

This will stop the bucket from ever hitting a cooldown and also prevents the bucket from defaulting.

Parameters
  • bucket_id (str): The bucket to disable.

    Note: "default" is a special bucket which is used as a template for unknown bucket IDs.

Returns
  • Self: This cooldown manager to allow for chaining.
#   def set_bucket( self: ~_InMemoryCooldownManagerT, bucket_id: str, resource: tanjun.dependencies.limiters.BucketResource, limit: int, reset_after: Union[int, float, datetime.timedelta], / ) -> ~_InMemoryCooldownManagerT:
View Source
    def set_bucket(
        self: _InMemoryCooldownManagerT,
        bucket_id: str,
        resource: BucketResource,
        limit: int,
        reset_after: typing.Union[int, float, datetime.timedelta],
        /,
    ) -> _InMemoryCooldownManagerT:
        """Set the cooldown for a specific bucket.

        Parameters
        ----------
        bucket_id : str
            The ID of the bucket to set the cooldown for.

            .. note::
                "default" is a special bucket which is used as a template
                for unknown bucket IDs.
        resource : tanjun.BucketResource
            The type of resource to target for the cooldown.
        limit : int
            The number of uses per cooldown period.
        reset_after : int, float, datetime.timedelta
            The cooldown period.

        Returns
        -------
        Self
            The cooldown manager to allow call chaining.

        Raises
        ------
        ValueError
            If an invalid resource type is given.
            If reset_after or limit are negative, 0 or invalid.
            if limit is less 0 or negative.
        """
        if isinstance(reset_after, datetime.timedelta):
            reset_after_seconds = reset_after.total_seconds()
        else:
            reset_after_seconds = float(reset_after)

        if reset_after_seconds <= 0:
            raise ValueError("reset_after must be greater than 0 seconds")

        if limit <= 0:
            raise ValueError("limit must be greater than 0")

        bucket = self._buckets[bucket_id] = _to_bucket(
            BucketResource(resource), lambda: _Cooldown(limit=limit, reset_after=reset_after_seconds)
        )
        if bucket_id == "default":
            self._default_bucket_template = bucket.copy()

        return self

Set the cooldown for a specific bucket.

Parameters
  • bucket_id (str): The ID of the bucket to set the cooldown for.

    Note: "default" is a special bucket which is used as a template for unknown bucket IDs.

  • resource (tanjun.BucketResource): The type of resource to target for the cooldown.
  • limit (int): The number of uses per cooldown period.
  • reset_after (int, float, datetime.timedelta): The cooldown period.
Returns
  • Self: The cooldown manager to allow call chaining.
Raises
  • ValueError: If an invalid resource type is given. If reset_after or limit are negative, 0 or invalid. if limit is less 0 or negative.
#   class LazyConstant(typing.Generic[~_T]):
View Source
class LazyConstant(typing.Generic[_T]):
    """Injected type used to hold and generate lazy constants.

    .. note::
        To easily resolve this type use `inject_lc`.
    """

    __slots__ = ("_callback", "_lock", "_value")

    def __init__(self, callback: collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]], /) -> None:
        """Initiate a new lazy constant.

        Parameters
        ----------
        callback : collections.abc.Callable[..., tanjun.abc.MaybeAwaitable[_T]]
            Callback used to resolve this to a constant value.

            This supports dependency injection and may either be sync or asynchronous.
        """
        self._callback = injecting.CallbackDescriptor(callback)
        self._lock: typing.Optional[asyncio.Lock] = None
        self._value: typing.Optional[_T] = None

    @property
    def callback(self) -> injecting.CallbackDescriptor[_T]:
        """Descriptor of the callback used to get this constant's initial value."""
        return self._callback

    def get_value(self) -> typing.Optional[_T]:
        """Get the value of this constant if set, else `None`."""
        return self._value

    def reset(self: _LazyConstantT) -> _LazyConstantT:
        """Clear the internally stored value."""
        self._value = None
        return self

    def set_value(self: _LazyConstantT, value: _T, /) -> _LazyConstantT:
        """Set the constant value.

        Parameters
        ----------
        value : _T
            The value to set.

        Raises
        ------
        RuntimeError
            If the constant has already been set.
        """
        if self._value is not None:
            raise RuntimeError("Constant value already set.")

        self._value = value
        self._lock = None
        return self

    def acquire(self) -> contextlib.AbstractAsyncContextManager[typing.Any]:
        """Acquire this lazy constant as an asynchronous lock.

        This is used to ensure that the value is only generated once
        and should be kept acquired until `LazyConstant.set_value` has
        been called.

        Returns
        -------
        contextlib.AbstractAsyncContextManager[typing.Any]
            Context manager that can be used to acquire the lock.
        """
        if not self._lock:
            # Error if this is called outside of a running event loop.
            asyncio.get_running_loop()
            self._lock = asyncio.Lock()

        return self._lock

Injected type used to hold and generate lazy constants.

Note: To easily resolve this type use inject_lc.

#   LazyConstant( callback: collections.abc.Callable[..., typing.Union[~_T, collections.abc.Awaitable[~_T]]], / )
View Source
    def __init__(self, callback: collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]], /) -> None:
        """Initiate a new lazy constant.

        Parameters
        ----------
        callback : collections.abc.Callable[..., tanjun.abc.MaybeAwaitable[_T]]
            Callback used to resolve this to a constant value.

            This supports dependency injection and may either be sync or asynchronous.
        """
        self._callback = injecting.CallbackDescriptor(callback)
        self._lock: typing.Optional[asyncio.Lock] = None
        self._value: typing.Optional[_T] = None

Initiate a new lazy constant.

Parameters
  • callback (collections.abc.Callable[..., tanjun.abc.MaybeAwaitable[_T]]): Callback used to resolve this to a constant value.

    This supports dependency injection and may either be sync or asynchronous.

Descriptor of the callback used to get this constant's initial value.

#   def get_value(self) -> Optional[~_T]:
View Source
    def get_value(self) -> typing.Optional[_T]:
        """Get the value of this constant if set, else `None`."""
        return self._value

Get the value of this constant if set, else None.

#   def reset(self: ~_LazyConstantT) -> ~_LazyConstantT:
View Source
    def reset(self: _LazyConstantT) -> _LazyConstantT:
        """Clear the internally stored value."""
        self._value = None
        return self

Clear the internally stored value.

#   def set_value(self: ~_LazyConstantT, value: ~_T, /) -> ~_LazyConstantT:
View Source
    def set_value(self: _LazyConstantT, value: _T, /) -> _LazyConstantT:
        """Set the constant value.

        Parameters
        ----------
        value : _T
            The value to set.

        Raises
        ------
        RuntimeError
            If the constant has already been set.
        """
        if self._value is not None:
            raise RuntimeError("Constant value already set.")

        self._value = value
        self._lock = None
        return self

Set the constant value.

Parameters
  • value (_T): The value to set.
Raises
  • RuntimeError: If the constant has already been set.
#   def acquire(self) -> contextlib.AbstractAsyncContextManager[typing.Any]:
View Source
    def acquire(self) -> contextlib.AbstractAsyncContextManager[typing.Any]:
        """Acquire this lazy constant as an asynchronous lock.

        This is used to ensure that the value is only generated once
        and should be kept acquired until `LazyConstant.set_value` has
        been called.

        Returns
        -------
        contextlib.AbstractAsyncContextManager[typing.Any]
            Context manager that can be used to acquire the lock.
        """
        if not self._lock:
            # Error if this is called outside of a running event loop.
            asyncio.get_running_loop()
            self._lock = asyncio.Lock()

        return self._lock

Acquire this lazy constant as an asynchronous lock.

This is used to ensure that the value is only generated once and should be kept acquired until LazyConstant.set_value has been called.

Returns
  • contextlib.AbstractAsyncContextManager[typing.Any]: Context manager that can be used to acquire the lock.
#   def with_concurrency_limit( bucket_id: str, /, *, error_message: str = 'This resource is currently busy; please try again later.' ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_concurrency_limit(
    bucket_id: str,
    /,
    *,
    error_message: str = "This resource is currently busy; please try again later.",
) -> collections.Callable[[CommandT], CommandT]:
    """Add the hooks used to manage a command's concurrency limit through a decorator call.

    .. warning::
        Concurrency limiters will only work if there's a setup injected
        `AbstractConcurrencyLimiter` dependency with `InMemoryConcurrencyLimiter`
        being usable as a standard in-memory concurrency manager.

    Parameters
    ----------
    bucket_id : str
        The concurrency limit bucket's ID.

    Other Parameters
    ----------------
    error_message : str
        The error message to send in response as a command error if this fails
        to acquire the concurrency limit.

        Defaults to "This resource is currently busy; please try again later.".

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A decorator that adds the concurrency limiter hooks to a command.
    """

    def decorator(command: CommandT, /) -> CommandT:
        hooks_ = command.hooks
        if not hooks_:
            hooks_ = hooks.AnyHooks()
            command.set_hooks(hooks_)

        hooks_.add_pre_execution(ConcurrencyPreExecution(bucket_id, error_message=error_message)).add_post_execution(
            ConcurrencyPostExecution(bucket_id)
        )
        return command

    return decorator

Add the hooks used to manage a command's concurrency limit through a decorator call.

Warning: Concurrency limiters will only work if there's a setup injected AbstractConcurrencyLimiter dependency with InMemoryConcurrencyLimiter being usable as a standard in-memory concurrency manager.

Parameters
  • bucket_id (str): The concurrency limit bucket's ID.
Other Parameters
  • error_message (str): The error message to send in response as a command error if this fails to acquire the concurrency limit.

    Defaults to "This resource is currently busy; please try again later.".

Returns
  • collections.abc.Callable[[CommandT], CommandT]: A decorator that adds the concurrency limiter hooks to a command.
#   def with_cooldown( bucket_id: str, /, *, error_message: str = 'Please wait {cooldown:0.2f} seconds before using this command again.', owners_exempt: bool = True ) -> collections.abc.Callable[[~CommandT], ~CommandT]:
View Source
def with_cooldown(
    bucket_id: str,
    /,
    *,
    error_message: str = "Please wait {cooldown:0.2f} seconds before using this command again.",
    owners_exempt: bool = True,
) -> collections.Callable[[CommandT], CommandT]:
    """Add a pre-execution hook used to manage a command's cooldown through a decorator call.

    .. warning::
        Cooldowns will only work if there's a setup injected `AbstractCooldownManager`
        dependency with `InMemoryCooldownManager` being usable as a standard in-memory
        cooldown manager.

    Parameters
    ----------
    bucket_id : str
        The cooldown bucket's ID.

    Other Parameters
    ----------------
    error_message : str
        The error message to send in response as a command error if the check fails.

        Defaults to f"Please wait {cooldown:0.2f} seconds before using this command again.".
    owners_exempt : bool
        Whether owners should be exempt from the cooldown.

        Defaults to `True`.

    Returns
    -------
    collections.abc.Callable[[CommandT], CommandT]
        A decorator that adds a `CooldownPreExecution` hook to the command.
    """

    def decorator(command: CommandT, /) -> CommandT:
        hooks_ = command.hooks
        if not hooks_:
            hooks_ = hooks.AnyHooks()
            command.set_hooks(hooks_)

        hooks_.add_pre_execution(
            CooldownPreExecution(bucket_id, error_message=error_message, owners_exempt=owners_exempt)
        )
        return command

    return decorator

Add a pre-execution hook used to manage a command's cooldown through a decorator call.

Warning: Cooldowns will only work if there's a setup injected AbstractCooldownManager dependency with InMemoryCooldownManager being usable as a standard in-memory cooldown manager.

Parameters
  • bucket_id (str): The cooldown bucket's ID.
Other Parameters
  • error_message (str): The error message to send in response as a command error if the check fails.

    Defaults to f"Please wait {cooldown:0.2f} seconds before using this command again.".

  • owners_exempt (bool): Whether owners should be exempt from the cooldown.

    Defaults to True.

Returns
  • collections.abc.Callable[[CommandT], CommandT]: A decorator that adds a CooldownPreExecution hook to the command.
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""The errors and warnings raised within and by Tanjun."""
from __future__ import annotations

__all__: list[str] = [
    "CommandError",
    "ConversionError",
    "FailedCheck",
    "FailedModuleLoad",
    "FailedModuleUnload",
    "HaltExecution",
    "MissingDependencyError",
    "ModuleMissingLoaders",
    "ModuleStateConflict",
    "NotEnoughArgumentsError",
    "TooManyArgumentsError",
    "ParserError",
    "TanjunError",
]

import typing

if typing.TYPE_CHECKING:
    import pathlib
    from collections import abc as collections


class TanjunError(Exception):
    """The base class for all errors raised by Tanjun."""

    __slots__ = ()


class HaltExecution(TanjunError):
    """Error raised while looking for a command in-order to end-execution early.

    For the most part, this will be raised during checks in-order to prevent
    other commands from being tried.
    """

    __slots__ = ()


class MissingDependencyError(TanjunError):
    """Error raised when a dependency couldn't be found."""

    __slots__ = ("message",)

    message: str
    """The error's message."""

    def __init__(self, message: str) -> None:
        """Initialise a missing dependency error.

        Parameters
        ----------
        message : str
            The error message.
        """
        self.message = message


class CommandError(TanjunError):
    """Error raised to end command execution."""

    __slots__ = ("message",)

    # None or empty string == no response
    message: str
    """The response error message.

    Tanjun will try to send the string message as a response.
    """

    def __init__(self, message: str, /) -> None:
        """Initialise a command error.

        Parameters
        ----------
        message : str
            String message which will be sent as a response to the message
            that triggered the current command.

        Raises
        ------
        ValueError
            Raised when the message is over 2000 characters long or empty.
        """
        if len(message) > 2000:
            raise ValueError("Error message cannot be over 2_000 characters long.")

        elif not message:
            raise ValueError("Response message must have at least 1 character.")

        self.message = message

    def __str__(self) -> str:
        return self.message or ""


# TODO: use this
class InvalidCheck(TanjunError, RuntimeError):  # TODO: or/and warning?  # TODO: InvalidCheckError
    """Error raised as an assertion that a check will never pass in the current environment."""

    __slots__ = ()


class FailedCheck(TanjunError, RuntimeError):  # TODO: FailedCheckError
    """Error raised as an alternative to returning `False` in a check."""

    __slots__ = ()


class ParserError(TanjunError, ValueError):
    """Base error raised by a parser or parameter during parsing.

    .. note::
        Expected errors raised by the parser will subclass this error.
    """

    __slots__ = ("message", "parameter")

    message: str
    """String message for this error.

    .. note::
        This may be used as a command response message.
    """

    parameter: typing.Optional[str]
    """Name of the this was raised for.

    .. note::
        This will be `builtin.None` if it was raised while parsing the provided
        message content.
    """

    def __init__(self, message: str, parameter: typing.Optional[str], /) -> None:
        """Initialise a parser error.

        Parameters
        ----------
        message : str
            String message for this error.
        parameter : typing.Optional[str]
            Name of the parameter which caused this error, should be `None` if not
            applicable.
        """
        self.message = message
        self.parameter = parameter

    def __str__(self) -> str:
        return self.message


class ConversionError(ParserError):
    """Error raised by a parser parameter when it failed to converter a value."""

    __slots__ = ("errors",)

    errors: collections.Sequence[ValueError]
    """Sequence of the errors that were caught during conversion for this parameter."""

    parameter: str
    """Name of the parameter this error was raised for."""

    def __init__(self, message: str, parameter: str, /, errors: collections.Iterable[ValueError] = ()) -> None:
        """Initialise a conversion error.

        Parameters
        ----------
        parameter : tanjun.abc.Parameter
            The parameter this was raised by.
        errors : collections.abc.Iterable[ValueError]
            An iterable of the source value errors which were raised during conversion.
        """
        super().__init__(message, parameter)
        self.errors = tuple(errors)


class NotEnoughArgumentsError(ParserError):
    """Error raised by the parser when not enough arguments are found for a parameter."""

    __slots__ = ()

    parameter: str
    """Name of the parameter this error was raised for."""

    def __init__(self, message: str, parameter: str, /) -> None:
        """Initialise a not enough arguments error.

        Parameters
        ----------
        message : str
            The error message.
        parameter : tanjun.abc.Parameter
            The parameter this error was raised for.
        """
        super().__init__(message, parameter)


class TooManyArgumentsError(ParserError):
    """Error raised by the parser when too many arguments are found for a parameter."""

    __slots__ = ()

    parameter: str
    """Name of the parameter this error was raised for."""

    def __init__(self, message: str, parameter: str, /) -> None:
        """Initialise a too many arguments error.

        Parameters
        ----------
        message : str
            The error message.
        parameter : tanjun.abc.Parameter
            The parameter this error was raised for.
        """
        super().__init__(message, parameter)


class ModuleMissingLoaders(RuntimeError, TanjunError):
    """Error raised when a module is missing loaders or unloaders."""

    __slots__ = ("_message", "_path")

    def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None:
        self._message = message
        self._path = path

    @property
    def message(self) -> str:
        """The error message."""
        return self._message

    @property
    def path(self) -> typing.Union[str, pathlib.Path]:
        """The path of the module which is missing loaders or unloaders."""
        return self._path


class ModuleStateConflict(ValueError, TanjunError):
    """Error raised when a module cannot be (un)loaded due to a state conflict."""

    __slots__ = ("_message", "_path")

    def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None:
        self._message = message
        self._path = path

    @property
    def message(self) -> str:
        """The error message."""
        return self._message

    @property
    def path(self) -> typing.Union[str, pathlib.Path]:
        """The path of the module which caused the error."""
        return self._path


class FailedModuleLoad(TanjunError):
    """Error raised when a module fails to load.

    This may be raised by the module failing to import or by one of
    its loaders erroring.

    This source error can be accessed at `FailedLoad.__cause__`.
    """

    __slots__ = ()

    __cause__: Exception
    """The root error."""


class FailedModuleUnload(TanjunError):
    """Error raised when a module fails to unload.

    This may be raised by the module failing to import or by one
    of its unloaders erroring.

    The source error can be accessed at `FailedUnload.__cause__`.
    """

    __slots__ = ()

    __cause__: Exception
    """The root error."""

The errors and warnings raised within and by Tanjun.

#   class CommandError(tanjun.TanjunError):
View Source
class CommandError(TanjunError):
    """Error raised to end command execution."""

    __slots__ = ("message",)

    # None or empty string == no response
    message: str
    """The response error message.

    Tanjun will try to send the string message as a response.
    """

    def __init__(self, message: str, /) -> None:
        """Initialise a command error.

        Parameters
        ----------
        message : str
            String message which will be sent as a response to the message
            that triggered the current command.

        Raises
        ------
        ValueError
            Raised when the message is over 2000 characters long or empty.
        """
        if len(message) > 2000:
            raise ValueError("Error message cannot be over 2_000 characters long.")

        elif not message:
            raise ValueError("Response message must have at least 1 character.")

        self.message = message

    def __str__(self) -> str:
        return self.message or ""

Error raised to end command execution.

#   CommandError(message: str, /)
View Source
    def __init__(self, message: str, /) -> None:
        """Initialise a command error.

        Parameters
        ----------
        message : str
            String message which will be sent as a response to the message
            that triggered the current command.

        Raises
        ------
        ValueError
            Raised when the message is over 2000 characters long or empty.
        """
        if len(message) > 2000:
            raise ValueError("Error message cannot be over 2_000 characters long.")

        elif not message:
            raise ValueError("Response message must have at least 1 character.")

        self.message = message

Initialise a command error.

Parameters
  • message (str): String message which will be sent as a response to the message that triggered the current command.
Raises
  • ValueError: Raised when the message is over 2000 characters long or empty.
#   message: str

The response error message.

Tanjun will try to send the string message as a response.

Inherited Members
builtins.BaseException
with_traceback
args
#   class ConversionError(tanjun.ParserError):
View Source
class ConversionError(ParserError):
    """Error raised by a parser parameter when it failed to converter a value."""

    __slots__ = ("errors",)

    errors: collections.Sequence[ValueError]
    """Sequence of the errors that were caught during conversion for this parameter."""

    parameter: str
    """Name of the parameter this error was raised for."""

    def __init__(self, message: str, parameter: str, /, errors: collections.Iterable[ValueError] = ()) -> None:
        """Initialise a conversion error.

        Parameters
        ----------
        parameter : tanjun.abc.Parameter
            The parameter this was raised by.
        errors : collections.abc.Iterable[ValueError]
            An iterable of the source value errors which were raised during conversion.
        """
        super().__init__(message, parameter)
        self.errors = tuple(errors)

Error raised by a parser parameter when it failed to converter a value.

#   ConversionError( message: str, parameter: str, /, errors: collections.abc.Iterable[ValueError] = () )
View Source
    def __init__(self, message: str, parameter: str, /, errors: collections.Iterable[ValueError] = ()) -> None:
        """Initialise a conversion error.

        Parameters
        ----------
        parameter : tanjun.abc.Parameter
            The parameter this was raised by.
        errors : collections.abc.Iterable[ValueError]
            An iterable of the source value errors which were raised during conversion.
        """
        super().__init__(message, parameter)
        self.errors = tuple(errors)

Initialise a conversion error.

Parameters
  • parameter (tanjun.abc.Parameter): The parameter this was raised by.
  • errors (collections.abc.Iterable[ValueError]): An iterable of the source value errors which were raised during conversion.
#   errors: collections.abc.Sequence[ValueError]

Sequence of the errors that were caught during conversion for this parameter.

#   parameter: str

Name of the parameter this error was raised for.

Inherited Members
ParserError
message
builtins.BaseException
with_traceback
args
#   class FailedCheck(tanjun.TanjunError, builtins.RuntimeError):
View Source
class FailedCheck(TanjunError, RuntimeError):  # TODO: FailedCheckError
    """Error raised as an alternative to returning `False` in a check."""

    __slots__ = ()

Error raised as an alternative to returning False in a check.

Inherited Members
builtins.RuntimeError
RuntimeError
builtins.BaseException
with_traceback
args
#   class FailedModuleLoad(tanjun.TanjunError):
View Source
class FailedModuleLoad(TanjunError):
    """Error raised when a module fails to load.

    This may be raised by the module failing to import or by one of
    its loaders erroring.

    This source error can be accessed at `FailedLoad.__cause__`.
    """

    __slots__ = ()

    __cause__: Exception
    """The root error."""

Error raised when a module fails to load.

This may be raised by the module failing to import or by one of its loaders erroring.

This source error can be accessed at FailedLoad.__cause__.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
#   class FailedModuleUnload(tanjun.TanjunError):
View Source
class FailedModuleUnload(TanjunError):
    """Error raised when a module fails to unload.

    This may be raised by the module failing to import or by one
    of its unloaders erroring.

    The source error can be accessed at `FailedUnload.__cause__`.
    """

    __slots__ = ()

    __cause__: Exception
    """The root error."""

Error raised when a module fails to unload.

This may be raised by the module failing to import or by one of its unloaders erroring.

The source error can be accessed at FailedUnload.__cause__.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
#   class HaltExecution(tanjun.TanjunError):
View Source
class HaltExecution(TanjunError):
    """Error raised while looking for a command in-order to end-execution early.

    For the most part, this will be raised during checks in-order to prevent
    other commands from being tried.
    """

    __slots__ = ()

Error raised while looking for a command in-order to end-execution early.

For the most part, this will be raised during checks in-order to prevent other commands from being tried.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
#   class MissingDependencyError(tanjun.TanjunError):
View Source
class MissingDependencyError(TanjunError):
    """Error raised when a dependency couldn't be found."""

    __slots__ = ("message",)

    message: str
    """The error's message."""

    def __init__(self, message: str) -> None:
        """Initialise a missing dependency error.

        Parameters
        ----------
        message : str
            The error message.
        """
        self.message = message

Error raised when a dependency couldn't be found.

#   MissingDependencyError(message: str)
View Source
    def __init__(self, message: str) -> None:
        """Initialise a missing dependency error.

        Parameters
        ----------
        message : str
            The error message.
        """
        self.message = message

Initialise a missing dependency error.

Parameters
  • message (str): The error message.
#   message: str

The error's message.

Inherited Members
builtins.BaseException
with_traceback
args
#   class ModuleMissingLoaders(builtins.RuntimeError, tanjun.TanjunError):
View Source
class ModuleMissingLoaders(RuntimeError, TanjunError):
    """Error raised when a module is missing loaders or unloaders."""

    __slots__ = ("_message", "_path")

    def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None:
        self._message = message
        self._path = path

    @property
    def message(self) -> str:
        """The error message."""
        return self._message

    @property
    def path(self) -> typing.Union[str, pathlib.Path]:
        """The path of the module which is missing loaders or unloaders."""
        return self._path

Error raised when a module is missing loaders or unloaders.

#   ModuleMissingLoaders(message: str, path: Union[str, pathlib.Path], /)
View Source
    def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None:
        self._message = message
        self._path = path
#   message: str

The error message.

#   path: Union[str, pathlib.Path]

The path of the module which is missing loaders or unloaders.

Inherited Members
builtins.BaseException
with_traceback
args
#   class ModuleStateConflict(builtins.ValueError, tanjun.TanjunError):
View Source
class ModuleStateConflict(ValueError, TanjunError):
    """Error raised when a module cannot be (un)loaded due to a state conflict."""

    __slots__ = ("_message", "_path")

    def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None:
        self._message = message
        self._path = path

    @property
    def message(self) -> str:
        """The error message."""
        return self._message

    @property
    def path(self) -> typing.Union[str, pathlib.Path]:
        """The path of the module which caused the error."""
        return self._path

Error raised when a module cannot be (un)loaded due to a state conflict.

#   ModuleStateConflict(message: str, path: Union[str, pathlib.Path], /)
View Source
    def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None:
        self._message = message
        self._path = path
#   message: str

The error message.

#   path: Union[str, pathlib.Path]

The path of the module which caused the error.

Inherited Members
builtins.BaseException
with_traceback
args
#   class NotEnoughArgumentsError(tanjun.ParserError):
View Source
class NotEnoughArgumentsError(ParserError):
    """Error raised by the parser when not enough arguments are found for a parameter."""

    __slots__ = ()

    parameter: str
    """Name of the parameter this error was raised for."""

    def __init__(self, message: str, parameter: str, /) -> None:
        """Initialise a not enough arguments error.

        Parameters
        ----------
        message : str
            The error message.
        parameter : tanjun.abc.Parameter
            The parameter this error was raised for.
        """
        super().__init__(message, parameter)

Error raised by the parser when not enough arguments are found for a parameter.

#   NotEnoughArgumentsError(message: str, parameter: str, /)
View Source
    def __init__(self, message: str, parameter: str, /) -> None:
        """Initialise a not enough arguments error.

        Parameters
        ----------
        message : str
            The error message.
        parameter : tanjun.abc.Parameter
            The parameter this error was raised for.
        """
        super().__init__(message, parameter)

Initialise a not enough arguments error.

Parameters
  • message (str): The error message.
  • parameter (tanjun.abc.Parameter): The parameter this error was raised for.
#   parameter: str

Name of the parameter this error was raised for.

Inherited Members
ParserError
message
builtins.BaseException
with_traceback
args
#   class TooManyArgumentsError(tanjun.ParserError):
View Source
class TooManyArgumentsError(ParserError):
    """Error raised by the parser when too many arguments are found for a parameter."""

    __slots__ = ()

    parameter: str
    """Name of the parameter this error was raised for."""

    def __init__(self, message: str, parameter: str, /) -> None:
        """Initialise a too many arguments error.

        Parameters
        ----------
        message : str
            The error message.
        parameter : tanjun.abc.Parameter
            The parameter this error was raised for.
        """
        super().__init__(message, parameter)

Error raised by the parser when too many arguments are found for a parameter.

#   TooManyArgumentsError(message: str, parameter: str, /)
View Source
    def __init__(self, message: str, parameter: str, /) -> None:
        """Initialise a too many arguments error.

        Parameters
        ----------
        message : str
            The error message.
        parameter : tanjun.abc.Parameter
            The parameter this error was raised for.
        """
        super().__init__(message, parameter)

Initialise a too many arguments error.

Parameters
  • message (str): The error message.
  • parameter (tanjun.abc.Parameter): The parameter this error was raised for.
#   parameter: str

Name of the parameter this error was raised for.

Inherited Members
ParserError
message
builtins.BaseException
with_traceback
args
#   class ParserError(tanjun.TanjunError, builtins.ValueError):
View Source
class ParserError(TanjunError, ValueError):
    """Base error raised by a parser or parameter during parsing.

    .. note::
        Expected errors raised by the parser will subclass this error.
    """

    __slots__ = ("message", "parameter")

    message: str
    """String message for this error.

    .. note::
        This may be used as a command response message.
    """

    parameter: typing.Optional[str]
    """Name of the this was raised for.

    .. note::
        This will be `builtin.None` if it was raised while parsing the provided
        message content.
    """

    def __init__(self, message: str, parameter: typing.Optional[str], /) -> None:
        """Initialise a parser error.

        Parameters
        ----------
        message : str
            String message for this error.
        parameter : typing.Optional[str]
            Name of the parameter which caused this error, should be `None` if not
            applicable.
        """
        self.message = message
        self.parameter = parameter

    def __str__(self) -> str:
        return self.message

Base error raised by a parser or parameter during parsing.

Note: Expected errors raised by the parser will subclass this error.

#   ParserError(message: str, parameter: Optional[str], /)
View Source
    def __init__(self, message: str, parameter: typing.Optional[str], /) -> None:
        """Initialise a parser error.

        Parameters
        ----------
        message : str
            String message for this error.
        parameter : typing.Optional[str]
            Name of the parameter which caused this error, should be `None` if not
            applicable.
        """
        self.message = message
        self.parameter = parameter

Initialise a parser error.

Parameters
  • message (str): String message for this error.
  • parameter (typing.Optional[str]): Name of the parameter which caused this error, should be None if not applicable.
#   message: str

String message for this error.

Note: This may be used as a command response message.

#   parameter: Optional[str]

Name of the this was raised for.

Note: This will be builtin.None if it was raised while parsing the provided message content.

Inherited Members
builtins.BaseException
with_traceback
args
#   class TanjunError(builtins.Exception):
View Source
class TanjunError(Exception):
    """The base class for all errors raised by Tanjun."""

    __slots__ = ()

The base class for all errors raised by Tanjun.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Standard implementation of Tanjun's command execution hook models."""
from __future__ import annotations

__all__: list[str] = ["AnyHooks", "Hooks", "MessageHooks", "SlashHooks"]

import asyncio
import copy
import typing
from collections import abc as collections

from . import abc
from . import errors
from . import injecting

if typing.TypeVar:
    _HooksT = typing.TypeVar("_HooksT", bound="Hooks[typing.Any]")

CommandT = typing.TypeVar("CommandT", bound=abc.ExecutableCommand[typing.Any])


class Hooks(abc.Hooks[abc.ContextT_contra]):
    """Standard implementation of `tanjun.abc.Hooks` used for command execution.

    `tanjun.abc.ContextT_contra` will either be `tanjun.abc.Context`,
    `tanjun.abc.MessageContext` or `tanjun.abc.SlashContext`.

    .. note::
        This implementation adds a concept of parser errors which won't be
        dispatched to general "error" hooks and do not share the error
        suppression semantics as they favour to always suppress the error
        if a registered handler is found.
    """

    __slots__ = (
        "_error_callbacks",
        "_parser_error_callbacks",
        "_pre_execution_callbacks",
        "_post_execution_callbacks",
        "_success_callbacks",
    )

    def __init__(self) -> None:
        """Initialise a command hook object."""
        self._error_callbacks: list[injecting.CallbackDescriptor[typing.Union[bool, None]]] = []
        self._parser_error_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._pre_execution_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._post_execution_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._success_callbacks: list[injecting.CallbackDescriptor[None]] = []

    def add_to_command(self, command: CommandT, /) -> CommandT:
        """Add this hook object to a command.

        .. note::
            This will likely override any previously added hooks.

        Examples
        --------
        This method may be used as a command decorator:

        ```py
        @standard_hooks.add_to_command
        @as_message_command("command")
        async def command_command(ctx: tanjun.abc.Context) -> None:
            await ctx.respond("You've called a command!")
        ```

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to add the hooks to.

        Returns
        -------
        tanjun.abc.ExecutableCommand[typing.Any]
            The command with the hooks added.
        """
        command.set_hooks(self)
        return command

    def copy(self: _HooksT) -> _HooksT:
        """Copy this hook object."""
        return copy.deepcopy(self)  # TODO: maybe don't

    def add_on_error(self: _HooksT, callback: abc.ErrorHookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._error_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_on_error(self: _HooksT, callback: typing.Optional[abc.ErrorHookSig], /) -> _HooksT:
        """Set the error callback for this hook object.

        .. note::
            This will not be called for `tanjun.errors.ParserError`s as these
            are generally speaking expected. To handle those see
            `Hooks.set_on_parser_error`.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.ErrorHookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `Exception`) and may be either
            synchronous or asynchronous.

            Returning `True` indicates that the error should be suppressed,
            `False` that it should be re-raised and `None` that no decision
            has been made. This will be accounted for along with the decisions
            other error hooks make by majority rule.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._error_callbacks.clear()
        return self.add_on_error(callback) if callback else self

    def with_on_error(self, callback: abc.ErrorHookSigT, /) -> abc.ErrorHookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_error(callback)
        return callback

    def add_on_parser_error(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._parser_error_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_on_parser_error(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the parser error callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`),
            return `None` and may be either synchronous or asynchronous.

            It's worth noting that, unlike general error handlers, this will
            always suppress the error.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._parser_error_callbacks.clear()
        return self.add_on_parser_error(callback) if callback else self

    def with_on_parser_error(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_parser_error(callback)
        return callback

    def add_post_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._post_execution_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_post_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the post-execution callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._post_execution_callbacks.clear()
        return self.add_post_execution(callback) if callback else self

    def with_post_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_post_execution(callback)
        return callback

    def add_pre_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._pre_execution_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_pre_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the pre-execution callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._pre_execution_callbacks.clear()
        return self.add_pre_execution(callback) if callback else self

    def with_pre_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_pre_execution(callback)
        return callback

    def add_on_success(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._success_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_on_success(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the success callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[HookSig[tanjun.abc.HookSig]]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._success_callbacks.clear()
        return self.add_on_success(callback) if callback else self

    def with_on_success(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_success(callback)
        return callback

    async def trigger_error(
        self,
        ctx: abc.ContextT_contra,
        /,
        exception: Exception,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> int:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        level = 0
        if isinstance(exception, errors.ParserError):
            if self._parser_error_callbacks:
                await asyncio.gather(
                    *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._parser_error_callbacks)
                )
                level = 100  # We don't want to re-raise a parser error if it was caught

        elif self._error_callbacks:
            results = await asyncio.gather(
                *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._error_callbacks)
            )
            level = results.count(True) - results.count(False)

        if hooks:
            level += sum(await asyncio.gather(*(hook.trigger_error(ctx, exception) for hook in hooks)))

        return level

    async def trigger_post_execution(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._post_execution_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._post_execution_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_post_execution(ctx) for hook in hooks))

    async def trigger_pre_execution(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._pre_execution_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._pre_execution_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_pre_execution(ctx) for hook in hooks))

    async def trigger_success(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._success_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._success_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_success(ctx) for hook in hooks))


AnyHooks = Hooks[abc.Context]
"""Hooks that can be used with any context.

.. note::
    This is shorthand for Hooks[tanjun.abc.Context].
"""

MessageHooks = Hooks[abc.MessageContext]
"""Hooks that can be used with a message context.

.. note::
    This is shorthand for Hooks[tanjun.abc.MessageContext].
"""

SlashHooks = Hooks[abc.SlashContext]
"""Hooks that can be used with a slash context.

.. note::
    This is shorthand for Hooks[tanjun.abc.SlashContext].
"""

Standard implementation of Tanjun's command execution hook models.

View Source
class Hooks(abc.Hooks[abc.ContextT_contra]):
    """Standard implementation of `tanjun.abc.Hooks` used for command execution.

    `tanjun.abc.ContextT_contra` will either be `tanjun.abc.Context`,
    `tanjun.abc.MessageContext` or `tanjun.abc.SlashContext`.

    .. note::
        This implementation adds a concept of parser errors which won't be
        dispatched to general "error" hooks and do not share the error
        suppression semantics as they favour to always suppress the error
        if a registered handler is found.
    """

    __slots__ = (
        "_error_callbacks",
        "_parser_error_callbacks",
        "_pre_execution_callbacks",
        "_post_execution_callbacks",
        "_success_callbacks",
    )

    def __init__(self) -> None:
        """Initialise a command hook object."""
        self._error_callbacks: list[injecting.CallbackDescriptor[typing.Union[bool, None]]] = []
        self._parser_error_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._pre_execution_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._post_execution_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._success_callbacks: list[injecting.CallbackDescriptor[None]] = []

    def add_to_command(self, command: CommandT, /) -> CommandT:
        """Add this hook object to a command.

        .. note::
            This will likely override any previously added hooks.

        Examples
        --------
        This method may be used as a command decorator:

        ```py
        @standard_hooks.add_to_command
        @as_message_command("command")
        async def command_command(ctx: tanjun.abc.Context) -> None:
            await ctx.respond("You've called a command!")
        ```

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to add the hooks to.

        Returns
        -------
        tanjun.abc.ExecutableCommand[typing.Any]
            The command with the hooks added.
        """
        command.set_hooks(self)
        return command

    def copy(self: _HooksT) -> _HooksT:
        """Copy this hook object."""
        return copy.deepcopy(self)  # TODO: maybe don't

    def add_on_error(self: _HooksT, callback: abc.ErrorHookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._error_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_on_error(self: _HooksT, callback: typing.Optional[abc.ErrorHookSig], /) -> _HooksT:
        """Set the error callback for this hook object.

        .. note::
            This will not be called for `tanjun.errors.ParserError`s as these
            are generally speaking expected. To handle those see
            `Hooks.set_on_parser_error`.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.ErrorHookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `Exception`) and may be either
            synchronous or asynchronous.

            Returning `True` indicates that the error should be suppressed,
            `False` that it should be re-raised and `None` that no decision
            has been made. This will be accounted for along with the decisions
            other error hooks make by majority rule.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._error_callbacks.clear()
        return self.add_on_error(callback) if callback else self

    def with_on_error(self, callback: abc.ErrorHookSigT, /) -> abc.ErrorHookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_error(callback)
        return callback

    def add_on_parser_error(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._parser_error_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_on_parser_error(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the parser error callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`),
            return `None` and may be either synchronous or asynchronous.

            It's worth noting that, unlike general error handlers, this will
            always suppress the error.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._parser_error_callbacks.clear()
        return self.add_on_parser_error(callback) if callback else self

    def with_on_parser_error(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_parser_error(callback)
        return callback

    def add_post_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._post_execution_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_post_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the post-execution callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._post_execution_callbacks.clear()
        return self.add_post_execution(callback) if callback else self

    def with_post_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_post_execution(callback)
        return callback

    def add_pre_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._pre_execution_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_pre_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the pre-execution callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._pre_execution_callbacks.clear()
        return self.add_pre_execution(callback) if callback else self

    def with_pre_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_pre_execution(callback)
        return callback

    def add_on_success(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._success_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

    def set_on_success(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the success callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[HookSig[tanjun.abc.HookSig]]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._success_callbacks.clear()
        return self.add_on_success(callback) if callback else self

    def with_on_success(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_success(callback)
        return callback

    async def trigger_error(
        self,
        ctx: abc.ContextT_contra,
        /,
        exception: Exception,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> int:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        level = 0
        if isinstance(exception, errors.ParserError):
            if self._parser_error_callbacks:
                await asyncio.gather(
                    *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._parser_error_callbacks)
                )
                level = 100  # We don't want to re-raise a parser error if it was caught

        elif self._error_callbacks:
            results = await asyncio.gather(
                *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._error_callbacks)
            )
            level = results.count(True) - results.count(False)

        if hooks:
            level += sum(await asyncio.gather(*(hook.trigger_error(ctx, exception) for hook in hooks)))

        return level

    async def trigger_post_execution(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._post_execution_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._post_execution_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_post_execution(ctx) for hook in hooks))

    async def trigger_pre_execution(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._pre_execution_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._pre_execution_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_pre_execution(ctx) for hook in hooks))

    async def trigger_success(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._success_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._success_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_success(ctx) for hook in hooks))

Standard implementation of tanjun.abc.Hooks used for command execution.

tanjun.abc.ContextT_contra will either be tanjun.abc.Context, tanjun.abc.MessageContext or tanjun.abc.SlashContext.

Note: This implementation adds a concept of parser errors which won't be dispatched to general "error" hooks and do not share the error suppression semantics as they favour to always suppress the error if a registered handler is found.

#   Hooks()
View Source
    def __init__(self) -> None:
        """Initialise a command hook object."""
        self._error_callbacks: list[injecting.CallbackDescriptor[typing.Union[bool, None]]] = []
        self._parser_error_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._pre_execution_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._post_execution_callbacks: list[injecting.CallbackDescriptor[None]] = []
        self._success_callbacks: list[injecting.CallbackDescriptor[None]] = []

Initialise a command hook object.

#   def add_to_command(self, command: ~CommandT, /) -> ~CommandT:
View Source
    def add_to_command(self, command: CommandT, /) -> CommandT:
        """Add this hook object to a command.

        .. note::
            This will likely override any previously added hooks.

        Examples
        --------
        This method may be used as a command decorator:

        ```py
        @standard_hooks.add_to_command
        @as_message_command("command")
        async def command_command(ctx: tanjun.abc.Context) -> None:
            await ctx.respond("You've called a command!")
        ```

        Parameters
        ----------
        command : tanjun.abc.ExecutableCommand[typing.Any]
            The command to add the hooks to.

        Returns
        -------
        tanjun.abc.ExecutableCommand[typing.Any]
            The command with the hooks added.
        """
        command.set_hooks(self)
        return command

Add this hook object to a command.

Note: This will likely override any previously added hooks.

Examples

This method may be used as a command decorator:

@standard_hooks.add_to_command
@as_message_command("command")
async def command_command(ctx: tanjun.abc.Context) -> None:
    await ctx.respond("You've called a command!")
Parameters
Returns
#   def copy(self: ~_HooksT) -> ~_HooksT:
View Source
    def copy(self: _HooksT) -> _HooksT:
        """Copy this hook object."""
        return copy.deepcopy(self)  # TODO: maybe don't

Copy this hook object.

#   def add_on_error( self: ~_HooksT, callback: collections.abc.Callable[..., typing.Union[bool, NoneType, collections.abc.Awaitable[typing.Optional[bool]]]], / ) -> ~_HooksT:
View Source
    def add_on_error(self: _HooksT, callback: abc.ErrorHookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._error_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

Add an error callback to this hook object.

Note: This won't be called for expected tanjun.TanjunError derived errors.

Parameters
  • callback (ErrorHookSig): The callback to add to this hook.

    This callback should take two positional arguments (of type tanjun.abc.ContextT_contra and Exception) and may be either synchronous or asynchronous.

    Returning True indicates that the error should be suppressed, False that it should be re-raised and None that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule.

Returns
  • Self: The hook object to enable method chaining.
#   def set_on_error( self: ~_HooksT, callback: Optional[collections.abc.Callable[..., Union[bool, NoneType, collections.abc.Awaitable[Optional[bool]]]]], / ) -> ~_HooksT:
View Source
    def set_on_error(self: _HooksT, callback: typing.Optional[abc.ErrorHookSig], /) -> _HooksT:
        """Set the error callback for this hook object.

        .. note::
            This will not be called for `tanjun.errors.ParserError`s as these
            are generally speaking expected. To handle those see
            `Hooks.set_on_parser_error`.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.ErrorHookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `Exception`) and may be either
            synchronous or asynchronous.

            Returning `True` indicates that the error should be suppressed,
            `False` that it should be re-raised and `None` that no decision
            has been made. This will be accounted for along with the decisions
            other error hooks make by majority rule.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._error_callbacks.clear()
        return self.add_on_error(callback) if callback else self

Set the error callback for this hook object.

Note: This will not be called for tanjun.errors.ParserErrors as these are generally speaking expected. To handle those see Hooks.set_on_parser_error.

Parameters
  • callback (typing.Optional[tanjun.abc.ErrorHookSig]): The callback to set for this hook. This will remove any previously set callbacks.

    This callback should take two positional arguments (of type tanjun.abc.ContextT_contra and Exception) and may be either synchronous or asynchronous.

    Returning True indicates that the error should be suppressed, False that it should be re-raised and None that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule.

Returns
  • Self: The hook object to enable method chaining.
#   def with_on_error(self, callback: ~ErrorHookSigT, /) -> ~ErrorHookSigT:
View Source
    def with_on_error(self, callback: abc.ErrorHookSigT, /) -> abc.ErrorHookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_error(callback)
        return callback

Add an error callback to this hook object through a decorator call.

Note: This won't be called for expected tanjun.TanjunError derived errors.

Examples
hooks = AnyHooks()

@hooks.with_on_error
async def on_error(ctx: tanjun.abc.Context, error: Exception) -> bool:
    if isinstance(error, SomeExpectedType):
        await ctx.respond("You dun goofed")
        return True  # Indicating that it should be suppressed.

    await ctx.respond(f"An error occurred: {error}")
    return False  # Indicating that it should be re-raised
Parameters
  • callback (ErrorHookSigT): The callback to add to this hook.

    This callback should take two positional arguments (of type tanjun.abc.ContextT_contra and Exception) and may be either synchronous or asynchronous.

    Returning True indicates that the error shoul be suppressed, False that it should be re-raised and None that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule.

Returns
  • ErrorHookSigT: The hook callback which was added.
#   def add_on_parser_error( self: ~_HooksT, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_HooksT:
View Source
    def add_on_parser_error(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._parser_error_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

Add a parser error callback to this hook object.

Parameters
  • callback (HookSig): The callback to add to this hook.

    This callback should take two positional arguments (of type tanjun.abc.ContextT_contra and tanjun.errors.ParserError), return None and may be either synchronous or asynchronous.

    It's worth noting that this unlike general error handlers, this will always suppress the error.

Returns
  • Self: The hook object to enable method chaining.
#   def set_on_parser_error( self: ~_HooksT, callback: Optional[collections.abc.Callable[..., Optional[collections.abc.Awaitable[NoneType]]]], / ) -> ~_HooksT:
View Source
    def set_on_parser_error(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the parser error callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take two positional arguments (of type
            `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`),
            return `None` and may be either synchronous or asynchronous.

            It's worth noting that, unlike general error handlers, this will
            always suppress the error.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._parser_error_callbacks.clear()
        return self.add_on_parser_error(callback) if callback else self

Set the parser error callback for this hook object.

Parameters
  • callback (typing.Optional[tanjun.abc.HookSig]): The callback to set for this hook. This will remove any previously set callbacks.

    This callback should take two positional arguments (of type tanjun.abc.ContextT_contra and tanjun.errors.ParserError), return None and may be either synchronous or asynchronous.

    It's worth noting that, unlike general error handlers, this will always suppress the error.

Returns
  • Self: The hook object to enable method chaining.
#   def with_on_parser_error(self, callback: ~HookSigT, /) -> ~HookSigT:
View Source
    def with_on_parser_error(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_parser_error(callback)
        return callback

Add a parser error callback to this hook object through a decorator call.

Examples
hooks = AnyHooks()

@hooks.with_on_parser_error
async def on_parser_error(ctx: tanjun.abc.Context, error: tanjun.errors.ParserError) -> None:
    await ctx.respond(f"You gave invalid input: {error}")
Parameters
Returns
  • HookSigT: The callback which was added.
#   def add_post_execution( self: ~_HooksT, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_HooksT:
View Source
    def add_post_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._post_execution_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

Add a post-execution callback to this hook object.

Parameters
  • callback (HookSig): The callback to add to this hook.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • Self: The hook object to enable method chaining.
#   def set_post_execution( self: ~_HooksT, callback: Optional[collections.abc.Callable[..., Optional[collections.abc.Awaitable[NoneType]]]], / ) -> ~_HooksT:
View Source
    def set_post_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the post-execution callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._post_execution_callbacks.clear()
        return self.add_post_execution(callback) if callback else self

Set the post-execution callback for this hook object.

Parameters
  • callback (typing.Optional[tanjun.abc.HookSig]): The callback to set for this hook. This will remove any previously set callbacks.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • Self: The hook object to enable method chaining.
#   def with_post_execution(self, callback: ~HookSigT, /) -> ~HookSigT:
View Source
    def with_post_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_post_execution(callback)
        return callback

Add a post-execution callback to this hook object through a decorator call.

Examples
hooks = AnyHooks()

@hooks.with_post_execution
async def post_execution(ctx: tanjun.abc.Context) -> None:
    await ctx.respond("You did something")
Parameters
  • callback (HookSigT): The post-execution callback to add to this hook.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • HookSigT: The post-execution callback which was seaddedt.
#   def add_pre_execution( self: ~_HooksT, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_HooksT:
View Source
    def add_pre_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._pre_execution_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

Add a pre-execution callback for this hook object.

Parameters
  • callback (HookSig): The callback to add to this hook.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • Self: The hook object to enable method chaining.
#   def set_pre_execution( self: ~_HooksT, callback: Optional[collections.abc.Callable[..., Optional[collections.abc.Awaitable[NoneType]]]], / ) -> ~_HooksT:
View Source
    def set_pre_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the pre-execution callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[tanjun.abc.HookSig]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._pre_execution_callbacks.clear()
        return self.add_pre_execution(callback) if callback else self

Set the pre-execution callback for this hook object.

Parameters
  • callback (typing.Optional[tanjun.abc.HookSig]): The callback to set for this hook. This will remove any previously set callbacks.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • Self: The hook object to enable method chaining.
#   def with_pre_execution(self, callback: ~HookSigT, /) -> ~HookSigT:
View Source
    def with_pre_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_pre_execution(callback)
        return callback

Add a pre-execution callback to this hook object through a decorator call.

Examples
hooks = AnyHooks()

@hooks.with_pre_execution
async def pre_execution(ctx: tanjun.abc.Context) -> None:
    await ctx.respond("You did something")
Parameters
  • callback (HookSigT): The pre-execution callback to add to this hook.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • HookSigT: The pre-execution callback which was added.
#   def add_on_success( self: ~_HooksT, callback: collections.abc.Callable[..., typing.Optional[collections.abc.Awaitable[NoneType]]], / ) -> ~_HooksT:
View Source
    def add_on_success(self: _HooksT, callback: abc.HookSig, /) -> _HooksT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self._success_callbacks.append(injecting.CallbackDescriptor(callback))
        return self

Add a success callback to this hook object.

Parameters
  • callback (HookSig): The callback to add to this hook.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • Self: The hook object to enable method chaining.
#   def set_on_success( self: ~_HooksT, callback: Optional[collections.abc.Callable[..., Optional[collections.abc.Awaitable[NoneType]]]], / ) -> ~_HooksT:
View Source
    def set_on_success(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT:
        """Set the success callback for this hook object.

        Parameters
        ----------
        callback : typing.Optional[HookSig[tanjun.abc.HookSig]]
            The callback to set for this hook. This will remove any previously
            set callbacks.

            This callback should take one positional argument (of type
            `tanjun.abc.ContextT_contra`), return `None` and may be either
            synchronous or asynchronous.

        Returns
        -------
        Self
            The hook object to enable method chaining.
        """
        self._success_callbacks.clear()
        return self.add_on_success(callback) if callback else self

Set the success callback for this hook object.

Parameters
  • callback (typing.Optional[HookSig[tanjun.abc.HookSig]]): The callback to set for this hook. This will remove any previously set callbacks.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • Self: The hook object to enable method chaining.
#   def with_on_success(self, callback: ~HookSigT, /) -> ~HookSigT:
View Source
    def with_on_success(self, callback: abc.HookSigT, /) -> abc.HookSigT:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        self.add_on_success(callback)
        return callback

Add a success callback to this hook object through a decorator call.

Examples
hooks = AnyHooks()

@hooks.with_on_success
async def on_success(ctx: tanjun.abc.Context) -> None:
    await ctx.respond("You did something")
Parameters
  • callback (HookSigT): The success callback to add to this hook.

    This callback should take one positional argument (of type tanjun.abc.ContextT_contra), return None and may be either synchronous or asynchronous.

Returns
  • HookSigT: The success callback which was added.
#   async def trigger_error( self, ctx: -ContextT_contra, /, exception: Exception, *, hooks: Optional[collections.abc.Set[tanjun.abc.Hooks[-ContextT_contra]]] = None ) -> int:
View Source
    async def trigger_error(
        self,
        ctx: abc.ContextT_contra,
        /,
        exception: Exception,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> int:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        level = 0
        if isinstance(exception, errors.ParserError):
            if self._parser_error_callbacks:
                await asyncio.gather(
                    *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._parser_error_callbacks)
                )
                level = 100  # We don't want to re-raise a parser error if it was caught

        elif self._error_callbacks:
            results = await asyncio.gather(
                *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._error_callbacks)
            )
            level = results.count(True) - results.count(False)

        if hooks:
            level += sum(await asyncio.gather(*(hook.trigger_error(ctx, exception) for hook in hooks)))

        return level
#   async def trigger_post_execution( self, ctx: -ContextT_contra, /, *, hooks: Optional[collections.abc.Set[tanjun.abc.Hooks[-ContextT_contra]]] = None ) -> None:
View Source
    async def trigger_post_execution(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._post_execution_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._post_execution_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_post_execution(ctx) for hook in hooks))
#   async def trigger_pre_execution( self, ctx: -ContextT_contra, /, *, hooks: Optional[collections.abc.Set[tanjun.abc.Hooks[-ContextT_contra]]] = None ) -> None:
View Source
    async def trigger_pre_execution(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._pre_execution_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._pre_execution_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_pre_execution(ctx) for hook in hooks))
#   async def trigger_success( self, ctx: -ContextT_contra, /, *, hooks: Optional[collections.abc.Set[tanjun.abc.Hooks[-ContextT_contra]]] = None ) -> None:
View Source
    async def trigger_success(
        self,
        ctx: abc.ContextT_contra,
        /,
        *,
        hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None,
    ) -> None:
        # <<inherited docstring from tanjun.abc.Hooks>>.
        if self._success_callbacks:
            await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._success_callbacks))

        if hooks:
            await asyncio.gather(*(hook.trigger_success(ctx) for hook in hooks))
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Logic and data classes used within the standard Tanjun implementation to enable dependency injection."""
from __future__ import annotations

__all__: list[str] = [
    "AbstractDescriptor",
    "AbstractInjectionContext",
    "as_self_injecting",
    "BasicInjectionContext",
    "CallbackDescriptor",
    "CallbackSig",
    "Undefined",
    "UNDEFINED",
    "UndefinedOr",
    "inject",
    "injected",
    "Injected",
    "InjectorClient",
    "SelfInjectingCallback",
    "TypeDescriptor",
]

import abc
import collections.abc as collections
import copy
import inspect
import sys
import types
import typing

from . import abc as tanjun_abc
from . import errors

if typing.TYPE_CHECKING:
    _BasicInjectionContextT = typing.TypeVar("_BasicInjectionContextT", bound="BasicInjectionContext")
    _CallbackDescriptorT = typing.TypeVar("_CallbackDescriptorT", bound="CallbackDescriptor[typing.Any]")
    _InjectorClientT = typing.TypeVar("_InjectorClientT", bound="InjectorClient")

_T = typing.TypeVar("_T")
CallbackSig = collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]]
"""Type-hint of a injector callback.

.. note::
    Dependency dependency injection is recursively supported, meaning that the
    keyword arguments for a dependency callback may also ask for dependencies
    themselves.

This may either be a synchronous or asynchronous function with dependency
injection being available for the callback's keyword arguments but dynamically
returning either an awaitable or raw value may lead to errors.

Dependent on the context positional arguments may also be proivded.
"""


class Undefined:
    """Class/type of `UNDEFINED`."""

    __instance: Undefined

    def __bool__(self) -> typing.Literal[False]:
        return False

    def __new__(cls) -> Undefined:
        try:
            return cls.__instance

        except AttributeError:
            new = super().__new__(cls)
            assert isinstance(new, Undefined)
            cls.__instance = new
            return cls.__instance


UNDEFINED: typing.Final[Undefined] = Undefined()
"""Singleton value used within dependency injection to indicate that a value is undefined."""
UndefinedOr = typing.Union[Undefined, _T]
"""Type-hint generic union used to indicate that a value may be undefined or `_T`."""


class AbstractInjectionContext(abc.ABC):
    """Abstract interface of an injection context."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def injection_client(self) -> InjectorClient:
        """Injection client this context is bound to."""

    @abc.abstractmethod
    def cache_result(self, callback: CallbackSig[_T], value: _T, /) -> None:
        """Cache the result of a callback within the scope of this context.

        Parameters
        ----------
        callback : CallbackSig[_T]
            The callback to cache the result of.
        value : _T
            The value to cache.
        """

    @abc.abstractmethod
    def get_cached_result(self, callback: CallbackSig[_T], /) -> UndefinedOr[_T]:
        """Get the cached result of a callback.

        Parameters
        ----------
        callback : CallbackSig[_T]
            The callback to get the cached result of.

        Returns
        -------
        UndefinedOr[_T]
            The cached result of the callback, or `UNDEFINED` if the callback
            has not been cached within this context.
        """

    @abc.abstractmethod
    def get_type_dependency(self, type_: type[_T], /) -> UndefinedOr[_T]:
        """Get the implementation for an injected type.

        .. note::
            Unlike `InjectionClient.get_type_dependency`, this method may also
            return context specific implementations of a type if the type isn't
            registered with the client.

        Parameters
        ----------
        type_: type[_T]
            The associated type.

        Returns
        -------
        UndefinedOr[_T]
            The resolved type if found, else `Undefined`.
        """


class BasicInjectionContext(AbstractInjectionContext):
    """Basic implementation of a `AbstractInjectionContext`."""

    __slots__ = ("_injection_client", "_result_cache", "_special_case_types")

    def __init__(self, client: InjectorClient, /) -> None:
        """Initialise a basic injection context.

        Parameters
        ----------
        client : InjectorClient
            The injection client this context is bound to.
        """
        self._injection_client = client
        self._result_cache: typing.Optional[dict[CallbackSig[typing.Any], typing.Any]] = None
        self._special_case_types: dict[type[typing.Any], typing.Any] = {
            AbstractInjectionContext: self,
            BasicInjectionContext: self,
            type(self): self,
        }

    @property
    def injection_client(self) -> InjectorClient:
        # <<inherited docstring from AbstractInjectionContext>>.
        return self._injection_client

    def cache_result(self, callback: CallbackSig[_T], value: _T, /) -> None:
        # <<inherited docstring from AbstractInjectionContext>>.
        if self._result_cache is None:
            self._result_cache = {}

        self._result_cache[callback] = value

    def get_cached_result(self, callback: CallbackSig[_T], /) -> UndefinedOr[_T]:
        # <<inherited docstring from AbstractInjectionContext>>.
        return self._result_cache.get(callback, UNDEFINED) if self._result_cache else UNDEFINED

    def get_type_dependency(self, type_: type[_T], /) -> UndefinedOr[_T]:
        # <<inherited docstring from AbstractInjectionContext>>.
        if (value := self._special_case_types.get(type_, UNDEFINED)) is not UNDEFINED:
            return value

        return self._injection_client.get_type_dependency(type_)

    def _set_type_special_case(self: _BasicInjectionContextT, type_: type[_T], value: _T, /) -> _BasicInjectionContextT:
        self._special_case_types[type_] = value
        return self

    def _remove_type_special_case(self: _BasicInjectionContextT, type_: type[typing.Any], /) -> _BasicInjectionContextT:
        del self._special_case_types[type_]
        return self


class AbstractDescriptor(abc.ABC, typing.Generic[_T]):
    """Abstract class for all injected argument descriptors."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def needs_injector(self) -> bool:
        """Whether this descriptor needs a dependency injection client to run."""

    @abc.abstractmethod
    async def resolve_with_command_context(self, ctx: tanjun_abc.Context, /) -> _T:
        """Try to resolve the descriptor with the given command context.

        Parameters
        ----------
        ctx : tanjun.abc.Context
            The context to resolve the descriptor with.

        Returns
        -------
        _T
            The result to be injected.

        Raises
        ------
        RuntimeError
            If the command context does not have a dependency injection client when
            one is required.
        tanjun.errors.MissingDependencyError
            If the client does not have an implementation of a non-defaulting
            type dependency this descriptor needs.
        """

    @abc.abstractmethod
    async def resolve_without_injector(self) -> _T:
        """Try to resolve this descriptor without a dependency injection client.

        Returns
        -------
        _T
            The result to be injected.

        Raises
        ------
        RuntimeError
            If a dependency injection client is required.
        tanjun.errors.MissingDependencyError
            If the client does not have an implementation of a non-defaulting
            type dependency this descriptor needs.
        """

    @abc.abstractmethod
    async def resolve(self, ctx: AbstractInjectionContext, /) -> _T:
        """Resolve the descriptor with the given dependency injection context.

        Parameters
        ----------
        ctx : tanjun.abc.AbstractInjectionContext
            The context to resolve the type or callback with.

        Returns
        -------
        _T
            The result to be injected.

        Raises
        ------
        tanjun.errors.MissingDependencyError
            If the client does not have an implementation of a non-defaulting
            type dependency this descriptor needs.
        """


class CallbackDescriptor(AbstractDescriptor[_T]):
    """Descriptor of a callback taking advantage of dependency injection.

    This holds metadata and logic necessary for callback injection.
    """

    __slots__ = ("_callback", "_descriptors", "_is_async", "_needs_injector")

    def __init__(self, callback: CallbackSig[_T], /) -> None:
        """Initialise an injected callback descriptor.

        Parameters
        ----------
        callback : CallbackSig[_T]
            The callback to wrap with dependency injection.

        Raises
        ------
        ValueError
            If `callback` has any injected arguments which can only be passed
            positionally.
        """
        self._callback = callback
        self._is_async: typing.Optional[bool] = None
        self._descriptors, self._needs_injector = self._parse_descriptors(callback)

    # This is delegated to the callback to delegate set/list behaviour for this class to the callback.
    def __eq__(self, other: typing.Any) -> bool:
        return bool(self._callback == other)

    # This is delegated to the callback to delegate set/list behaviour for this class to the callback.
    def __hash__(self) -> int:
        return hash(self._callback)

    @property
    def callback(self) -> CallbackSig[_T]:
        """The descriptor's callback."""
        return self._callback

    @property
    def needs_injector(self) -> bool:
        # <<inherited docstring from Descriptor>>.
        return self._needs_injector

    @staticmethod
    def _parse_descriptors(callback: CallbackSig[_T], /) -> tuple[dict[str, AbstractDescriptor[typing.Any]], bool]:
        try:
            parameters = inspect.signature(callback).parameters.items()
        except ValueError:  # If we can't inspect it then we have to assume this is a NO
            # As a note, this fails on some "signature-less" builtin functions/types like str.
            return {}, False

        descriptors: dict[str, AbstractDescriptor[typing.Any]] = {}
        for name, parameter in parameters:
            if parameter.default is parameter.empty or not isinstance(parameter.default, Injected):
                continue

            if parameter.kind is parameter.POSITIONAL_ONLY:
                raise ValueError("Injected positional only arguments are not supported")

            if parameter.default.callback is not None:
                descriptors[name] = CallbackDescriptor(parameter.default.callback)

            else:
                assert parameter.default.type is not None
                descriptors[name] = TypeDescriptor(typing.cast("type[_T]", parameter.default.type))

        return descriptors, any(d.needs_injector for d in descriptors.values())

    def copy(self: _CallbackDescriptorT, *, _new: bool = True) -> _CallbackDescriptorT:
        """Create a copy of this descriptor.

        Returns
        -------
        CallbackDescriptor[_T]
            A copy of this descriptor.
        """
        if not _new:
            self._callback = copy.copy(self._callback)
            return self

        return copy.copy(self).copy(_new=False)

    def overwrite_callback(self, callback: CallbackSig[_T], /) -> None:
        """Overwrite the callback of this descriptor.

        Parameters
        ----------
        callback : CallbackSig[_T]
            The new callback to overwrite with.

        Raises
        ------
        ValueError
            If `callback` has any injected arguments which can only be passed
            positionally.
        """
        self._callback = callback
        self._is_async = None
        self._descriptors, self._needs_injector = self._parse_descriptors(callback)

    def resolve_with_command_context(
        self, ctx: tanjun_abc.Context, /, *args: typing.Any, **kwargs: typing.Any
    ) -> collections.Coroutine[typing.Any, typing.Any, _T]:
        """Try to resolve the callback with the given command context.

        Parameters
        ----------
        ctx : tanjun.abc.Context
            The context to resolve the callback with.
        *args : typing.Any
            The positional arguments to pass to the callback.
        **kwargs : typing.Any
            The keyword arguments to pass to the callback.

        Returns
        -------
        _T
            The callback's result.

        Raises
        ------
        RuntimeError
            If the callback needs a dependency injection client but the
            context does not have one.
        tanjun.errors.MissingDependencyError
            If the callback needs an injected type which isn't present in the
            context or client and doesn't have a set default.
        """
        if self.needs_injector and isinstance(ctx, AbstractInjectionContext):
            return self.resolve(ctx, *args, **kwargs)

        return self.resolve_without_injector(*args, **kwargs)

    def resolve_without_injector(
        self, *args: typing.Any, **kwargs: typing.Any
    ) -> collections.Coroutine[typing.Any, typing.Any, _T]:
        """Try to resolve the callback without a dependency injection client.

        Parameters
        ----------
        *args : typing.Any
            The positional arguments to pass to the callback.
        **kwargs : typing.Any
            The keyword arguments to pass to the callback.

        Returns
        -------
        _T
            The callback's result.

        Raises
        ------
        RuntimeError
            If the callback needs a dependency injection client present.
        """
        if self._needs_injector:
            raise RuntimeError("Callback descriptor needs a dependency injection client")

        return self.resolve(_EmptyContext(), *args, **kwargs)

    async def resolve(self, ctx: AbstractInjectionContext, /, *args: typing.Any, **kwargs: typing.Any) -> _T:
        """Resolve the callback with the given dependency injection context.

        Parameters
        ----------
        ctx : AbstractInjectionContext
            The context to resolve the callback with.
        *args : typing.Any
            The positional arguments to pass to the callback.
        **kwargs : typing.Any
            The keyword arguments to pass to the callback.

        Returns
        -------
        _T
            The callback's result.

        Raises
        ------
        tanjun.errors.MissingDependencyError
            If the callback needs an injected type which isn't present in the
            context or client and doesn't have a set default.
        """
        if override := ctx.injection_client.get_callback_override(self._callback):
            return await override.resolve(ctx, *args, **kwargs)

        if (result := ctx.get_cached_result(self._callback)) is not UNDEFINED:
            assert not isinstance(result, Undefined)
            return result

        sub_results = {name: await descriptor.resolve(ctx) for name, descriptor in self._descriptors.items()}
        result = self._callback(*args, **sub_results, **kwargs)

        if self._is_async is None:
            self._is_async = inspect.isawaitable(result)

        if self._is_async:
            assert inspect.isawaitable(result)
            result = await result

        # TODO: should we avoid caching the result if args/kwargs are passed?
        ctx.cache_result(self._callback, result)
        return typing.cast(_T, result)


class SelfInjectingCallback(CallbackDescriptor[_T]):
    """Class used to make a callback self-injecting by linking it to a client.

    Examples
    --------
    ```py
    async def callback(database: Database = tanjun.inject(type=Database)) -> None:
        await database.do_something()

    ...

    client = tanjun.Client.from_gateway_bot(bot)
    injecting_callback = tanjun.SelfInjectingCallback(callback, client)
    await injecting_callback()
    ```
    """

    __slots__ = ("_client",)

    def __init__(self, injector_client: InjectorClient, callback: CallbackSig[_T], /) -> None:
        """Initialise a self injecting callback.

        Parameters
        ----------
        injector : InjectorClient
            The injection client to use to resolve dependencies.
        callback : CallbackSig[_T]
            The callback to make self-injecting.

        Raises
        ------
        ValueError
            If `callback` has any injected arguments which can only be passed
            positionally.
        """
        super().__init__(callback)
        self._client = injector_client

    async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> _T:
        """Call this callback with the provided arguments + injected arguments.

        Parameters
        ----------
        *args : typing.Any
            The positional arguments to pass to the callback.
        **kwargs : typing.Any
            The keyword arguments to pass to the callback.

        Returns
        -------
        _T
            The callback's result.
        """
        ctx = BasicInjectionContext(self._client)
        return await self.resolve(ctx, *args, **kwargs)


def as_self_injecting(
    injector_client: InjectorClient, /
) -> collections.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]:
    """Make a callback self-inecting by linking it to a client through a decorator call.

    Examples
    --------
    ```py
    def make_callback(client: tanjun.Client) -> collections.abc.Callable[[], int]:
        @tanjun.as_self_injected(client)
        async def get_int_value(
            redis: redis.Client = tanjun.inject(type=redis.Client)
        ) -> int:
            return int(await redis.get('key'))

        return get_int_value
    ```

    Parameters
    ----------
    injector_client : InjectorClient
        The injection client to use to resolve dependencies.

    Returns
    -------
    collections.abc.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]
    """

    def decorator(callback: CallbackSig[_T], /) -> SelfInjectingCallback[_T]:
        return SelfInjectingCallback(injector_client, callback)

    return decorator


if sys.version_info >= (3, 10):
    _UnionTypes = {typing.Union, types.UnionType}
    _NoneType = types.NoneType

else:
    _UnionTypes = {typing.Union}
    _NoneType = type(None)


class TypeDescriptor(AbstractDescriptor[_T]):
    """Descriptor of an injected type.

    This class holds all the logic for resolving a type with dependency
    injection.
    """

    __slots__ = ("_default", "_type", "_union")

    def __init__(self, type_: _TypeT[_T], /) -> None:
        """Initialise an injected type descriptor.

        Parameters
        ----------
        type_ : type[_T]
            The type to resolve.
        """
        self._default: UndefinedOr[_T] = UNDEFINED
        self._type = type_
        self._union: typing.Optional[list[type[_T]]] = None

        if typing.get_origin(type_) not in _UnionTypes:
            return

        sub_types = list(typing.get_args(type_))
        try:
            sub_types.remove(_NoneType)
        except ValueError:
            pass
        else:
            self._default = typing.cast(_T, None)

        self._union = sub_types

    @property
    def needs_injector(self) -> bool:
        # <<inherited docstring from AbstractDescriptor>>.
        return self._default is UNDEFINED

    @property
    def type(self) -> _TypeT[_T]:
        """The type being injected."""
        return self._type  # type: ignore  # pyright bug?

    def resolve_with_command_context(
        self, ctx: tanjun_abc.Context, /
    ) -> collections.Coroutine[typing.Any, typing.Any, _T]:
        # <<inherited docstring from AbstractDescriptor>>.
        if self.needs_injector and isinstance(ctx, AbstractInjectionContext):
            return self.resolve(ctx)

        return self.resolve_without_injector()

    async def resolve_without_injector(self) -> _T:
        # <<inherited docstring from AbstractDescriptor>>.
        if self._default is not UNDEFINED:
            assert not isinstance(self._default, Undefined)
            return self._default

        raise RuntimeError("Type descriptor cannot be resolved without an injection client")

    async def resolve(self, ctx: AbstractInjectionContext, /) -> _T:
        # <<inherited docstring from AbstractDescriptor>>.
        if (result := ctx.get_type_dependency(self._type)) is not UNDEFINED:
            assert not isinstance(result, Undefined)
            return result

        # We still want to allow for the possibility of a Union being
        # explicitly implemented so we check types within a union
        # after the literal type.
        if self._union:
            for cls in self._union:
                if (result := ctx.get_type_dependency(cls)) is not UNDEFINED:
                    assert not isinstance(result, Undefined)
                    return result

        if self._default is not UNDEFINED:
            assert not isinstance(self._default, Undefined)
            return self._default

        raise errors.MissingDependencyError(f"Couldn't resolve injected type {self._type} to actual value") from None


_TypeT = type[_T]


class Injected(typing.Generic[_T]):
    """Decare a keyword-argument as requiring an injected dependency.

    This is the type returned by `inject`.
    """

    __slots__ = ("callback", "type")

    def __init__(
        self,
        *,
        callback: typing.Optional[CallbackSig[_T]] = None,
        type: typing.Optional[_TypeT[_T]] = None,  # noqa: A002
    ) -> None:  # TODO: add default/factory to this?
        """Initialise an injection default descriptor.

        Parameters
        ----------
        callback : typing.Optional[CallbackSig[_T]]
            The callback to use to resolve the dependency.

            If this callback has no type dependencies then this will still work
            without an injection context but this can be overridden using
            `InjectionClient.set_callback_override`.
        type : typing.Optional[type[_T]]
            The type of the dependency to resolve.

            If a union (e.g. `typing.Union[A, B]`, `A | B`, `typing.Optional[A]`)
            is passed for `type` then each type in the union will be tried
            separately after the litarl union type is tried, allowing for resolving
            `A | B` to the value set by `set_type_dependency(B, ...)`.

            If a union has `None` as one of its types (including `Optional[T]`)
            then `None` will be passed for the parameter if none of the types could
            be resolved using the linked client.

        Raises
        ------
        ValueError
            If both `callback` and `type` are specified or if neither is specified.
        """
        if callback is None and type is None:
            raise ValueError("Must specify one of `callback` or `type`")

        if callback is not None and type is not None:
            raise ValueError("Only one of `callback` or `type` can be specified")

        self.callback = callback
        self.type = type


def inject(
    *,
    callback: typing.Optional[CallbackSig[_T]] = None,
    type: typing.Optional[_TypeT[_T]] = None,  # noqa: A002
) -> Injected[_T]:
    """Decare a keyword-argument as requiring an injected dependency.

    This should be assigned to an arugment's default value.

    Examples
    --------
    ```py
    @tanjun.as_slash_command("name", "description")
    async def command_callback(
        ctx: tanjun.abc.Context,
        # Here we take advantage of scope based special casing which allows
        # us to inject the `Component` type.
        injected_type: tanjun.abc.Component = tanjun.inject(type=tanjun.abc.Component)
        # Here we inject an out-of-scope callback which itself is taking
        # advantage of type injection.
        callback_result: ResultT = tanjun.inject(callback=injected_callback)
    ) -> None:
        raise NotImplementedError
    ```

    Parameters
    ----------
    callback : typing.Optional[CallbackSig[_T]]
        The callback to use to resolve the dependency.

        If this callback has no type dependencies then this will still work
        without an injection context but this can be overridden using
        `InjectionClient.set_callback_override`.
    type : typing.Optional[type[_T]]
        The type of the dependency to resolve.

        If a union (e.g. `typing.Union[A, B]`, `A | B`, `typing.Optional[A]`)
        is passed for `type` then each type in the union will be tried
        separately after the litarl union type is tried, allowing for resolving
        `A | B` to the value set by `set_type_dependency(B, ...)`.

        If a union has `None` as one of its types (including `Optional[T]`)
        then `None` will be passed for the parameter if none of the types could
        be resolved using the linked client.

    Raises
    ------
    ValueError
        If both `callback` and `type` are specified or if neither is specified.
    """
    return Injected(callback=callback, type=type)


def injected(
    *,
    callback: typing.Optional[CallbackSig[_T]] = None,
    type: typing.Optional[_TypeT[_T]] = None,  # noqa: A002
) -> Injected[_T]:
    """Alias of `inject`."""
    return inject(callback=callback, type=type)


class InjectorClient:
    """Dependency injection client used by Tanjun's standard implementation."""

    __slots__ = ("_callback_overrides", "_type_dependencies")

    def __init__(self) -> None:
        """Initialise an injector client."""
        self._callback_overrides: dict[CallbackSig[typing.Any], CallbackDescriptor[typing.Any]] = {}
        self._type_dependencies: dict[type[typing.Any], typing.Any] = {InjectorClient: self}

    def set_type_dependency(self: _InjectorClientT, type_: type[_T], value: _T, /) -> _InjectorClientT:
        """Set a callback to be called to resolve a injected type.

        Parameters
        ----------
        callback: CallbackSig[_T]
            The callback to use to resolve the dependency.

            If this callback has no type dependencies then this will still work
            without an injection context but this can be overridden using
            `InjectionClient.set_callback_override`.
        type_: type[_T]
            The type of the dependency to resolve.

        Returns
        -------
        Self
            The client instance to allow chaining.
        """
        self._type_dependencies[type_] = value
        return self

    def get_type_dependency(self, type_: type[_T], /) -> UndefinedOr[_T]:
        """Get the implementation for an injected type.

        Parameters
        ----------
        type_: type[_T]
            The associated type.

        Returns
        -------
        UndefinedOr[_T]
            The resolved type if found, else `Undefined`.
        """
        return self._type_dependencies.get(type_, UNDEFINED)

    def remove_type_dependency(self: _InjectorClientT, type_: type[typing.Any], /) -> _InjectorClientT:
        """Remove a type dependency.

        Parameters
        ----------
        type_: type[_T]
            The associated type.

        Returns
        -------
        Self
            The client instance to allow chaining.

        Raises
        ------
        KeyError
            If `type_` is not registered.
        """
        del self._type_dependencies[type_]
        return self

    def set_callback_override(
        self: _InjectorClientT, callback: CallbackSig[_T], override: CallbackSig[_T], /
    ) -> _InjectorClientT:
        """Override a specific injected callback.

        .. note::
            This does not effect the callbacks set for type injectors.

        Parameters
        ----------
        callback: CallbackSig[_T]
            The injected callback to override.
        override: CallbackSig[_T]
            The callback to use instead.

        Returns
        -------
        Self
            The client instance to allow chaining.
        """
        self._callback_overrides[callback] = CallbackDescriptor(override)
        return self

    def get_callback_override(self, callback: CallbackSig[_T], /) -> typing.Optional[CallbackDescriptor[_T]]:
        """Get the set override for a specific injected callback.

        Parameters
        ----------
        callback: CallbackSig[_T]
            The injected callback to get the override for.

        Returns
        -------
        typing.Optional[CallbackDescriptor[_T]]
            The override if found, else `None`.
        """
        return self._callback_overrides.get(callback)

    def remove_callback_override(self: _InjectorClientT, callback: CallbackSig[_T], /) -> _InjectorClientT:
        """Remove a callback override.

        Parameters
        ----------
        callback: CallbackSig[_T]
            The injected callback to remove the override for.

        Returns
        -------
        Self
            The client instance to allow chaining.

        Raises
        ------
        KeyError
            If no override is found for the callback.
        """
        del self._callback_overrides[callback]
        return self


class _EmptyInjectorClient(InjectorClient):
    __slots__ = ()

    def set_type_dependency(self: _InjectorClientT, _: type[_T], __: _T, /) -> _InjectorClientT:
        return self  # NOOP is safer here than NotImplementedError

    def get_type_dependency(self, _: type[typing.Any], /) -> Undefined:
        return UNDEFINED

    def remove_type_dependency(self: _InjectorClientT, type_: type[typing.Any], /) -> _InjectorClientT:
        raise KeyError(type_)

    def set_callback_override(self: _InjectorClientT, _: CallbackSig[_T], __: CallbackSig[_T], /) -> _InjectorClientT:
        return self  # NOOP is safer here than NotImplementedError

    def get_callback_override(self, _: CallbackSig[_T], /) -> None:
        return

    def remove_callback_override(self: _InjectorClientT, callback: CallbackSig[_T], /) -> _InjectorClientT:
        raise KeyError(callback)


_EMPTY_CLIENT = _EmptyInjectorClient()


class _EmptyContext(AbstractInjectionContext):
    __slots__ = ("_result_cache",)

    def __init__(self) -> None:
        self._result_cache: typing.Optional[dict[CallbackSig[typing.Any], typing.Any]] = None

    @property
    def injection_client(self) -> InjectorClient:
        return _EMPTY_CLIENT

    def cache_result(self, callback: CallbackSig[_T], value: _T, /) -> None:
        if self._result_cache is None:
            self._result_cache = {}

        self._result_cache[callback] = value

    def get_cached_result(self, callback: CallbackSig[typing.Any], /) -> Undefined:
        return self._result_cache.get(callback, UNDEFINED) if self._result_cache else UNDEFINED

    def get_type_dependency(self, _: type[typing.Any], /) -> Undefined:
        return UNDEFINED

Logic and data classes used within the standard Tanjun implementation to enable dependency injection.

#   def as_self_injecting( injector_client: tanjun.injecting.InjectorClient, / ) -> collections.abc.Callable[[collections.abc.Callable[..., typing.Union[~_T, collections.abc.Awaitable[~_T]]]], tanjun.injecting.SelfInjectingCallback[~_T]]:
View Source
def as_self_injecting(
    injector_client: InjectorClient, /
) -> collections.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]:
    """Make a callback self-inecting by linking it to a client through a decorator call.

    Examples
    --------
    ```py
    def make_callback(client: tanjun.Client) -> collections.abc.Callable[[], int]:
        @tanjun.as_self_injected(client)
        async def get_int_value(
            redis: redis.Client = tanjun.inject(type=redis.Client)
        ) -> int:
            return int(await redis.get('key'))

        return get_int_value
    ```

    Parameters
    ----------
    injector_client : InjectorClient
        The injection client to use to resolve dependencies.

    Returns
    -------
    collections.abc.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]
    """

    def decorator(callback: CallbackSig[_T], /) -> SelfInjectingCallback[_T]:
        return SelfInjectingCallback(injector_client, callback)

    return decorator

Make a callback self-inecting by linking it to a client through a decorator call.

Examples
def make_callback(client: tanjun.Client) -> collections.abc.Callable[[], int]:
    @tanjun.as_self_injected(client)
    async def get_int_value(
        redis: redis.Client = tanjun.inject(type=redis.Client)
    ) -> int:
        return int(await redis.get('key'))

    return get_int_value
Parameters
  • injector_client (InjectorClient): The injection client to use to resolve dependencies.
Returns
  • collections.abc.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]
#   def inject( *, callback: Optional[collections.abc.Callable[..., Union[~_T, collections.abc.Awaitable[~_T]]]] = None, type: Optional[type[~_T]] = None ) -> tanjun.injecting.Injected[~_T]:
View Source
def inject(
    *,
    callback: typing.Optional[CallbackSig[_T]] = None,
    type: typing.Optional[_TypeT[_T]] = None,  # noqa: A002
) -> Injected[_T]:
    """Decare a keyword-argument as requiring an injected dependency.

    This should be assigned to an arugment's default value.

    Examples
    --------
    ```py
    @tanjun.as_slash_command("name", "description")
    async def command_callback(
        ctx: tanjun.abc.Context,
        # Here we take advantage of scope based special casing which allows
        # us to inject the `Component` type.
        injected_type: tanjun.abc.Component = tanjun.inject(type=tanjun.abc.Component)
        # Here we inject an out-of-scope callback which itself is taking
        # advantage of type injection.
        callback_result: ResultT = tanjun.inject(callback=injected_callback)
    ) -> None:
        raise NotImplementedError
    ```

    Parameters
    ----------
    callback : typing.Optional[CallbackSig[_T]]
        The callback to use to resolve the dependency.

        If this callback has no type dependencies then this will still work
        without an injection context but this can be overridden using
        `InjectionClient.set_callback_override`.
    type : typing.Optional[type[_T]]
        The type of the dependency to resolve.

        If a union (e.g. `typing.Union[A, B]`, `A | B`, `typing.Optional[A]`)
        is passed for `type` then each type in the union will be tried
        separately after the litarl union type is tried, allowing for resolving
        `A | B` to the value set by `set_type_dependency(B, ...)`.

        If a union has `None` as one of its types (including `Optional[T]`)
        then `None` will be passed for the parameter if none of the types could
        be resolved using the linked client.

    Raises
    ------
    ValueError
        If both `callback` and `type` are specified or if neither is specified.
    """
    return Injected(callback=callback, type=type)

Decare a keyword-argument as requiring an injected dependency.

This should be assigned to an arugment's default value.

Examples
@tanjun.as_slash_command("name", "description")
async def command_callback(
    ctx: tanjun.abc.Context,
    # Here we take advantage of scope based special casing which allows
    # us to inject the `Component` type.
    injected_type: tanjun.abc.Component = tanjun.inject(type=tanjun.abc.Component)
    # Here we inject an out-of-scope callback which itself is taking
    # advantage of type injection.
    callback_result: ResultT = tanjun.inject(callback=injected_callback)
) -> None:
    raise NotImplementedError
Parameters
  • callback (typing.Optional[CallbackSig[_T]]): The callback to use to resolve the dependency.

    If this callback has no type dependencies then this will still work without an injection context but this can be overridden using InjectionClient.set_callback_override.

  • type (typing.Optional[type[_T]]): The type of the dependency to resolve.

    If a union (e.g. typing.Union[A, B], A | B, typing.Optional[A]) is passed for type then each type in the union will be tried separately after the litarl union type is tried, allowing for resolving A | B to the value set by set_type_dependency(B, ...).

    If a union has None as one of its types (including Optional[T]) then None will be passed for the parameter if none of the types could be resolved using the linked client.

Raises
  • ValueError: If both callback and type are specified or if neither is specified.
#   def injected( *, callback: Optional[collections.abc.Callable[..., Union[~_T, collections.abc.Awaitable[~_T]]]] = None, type: Optional[type[~_T]] = None ) -> tanjun.injecting.Injected[~_T]:
View Source
def injected(
    *,
    callback: typing.Optional[CallbackSig[_T]] = None,
    type: typing.Optional[_TypeT[_T]] = None,  # noqa: A002
) -> Injected[_T]:
    """Alias of `inject`."""
    return inject(callback=callback, type=type)

Alias of inject.

View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Standard implementation of message command argument parsing."""
from __future__ import annotations

__all__: list[str] = [
    "AbstractOptionParser",
    "Argument",
    "ConverterSig",
    "Option",
    "Parameter",
    "ShlexParser",
    "UndefinedT",
    "UNDEFINED",
    "with_argument",
    "with_greedy_argument",
    "with_multi_argument",
    "with_option",
    "with_multi_option",
    "with_parser",
]

import abc
import asyncio
import copy
import itertools
import shlex
import typing
from collections import abc as collections

from . import abc as tanjun_abc
from . import conversion
from . import errors
from . import injecting

if typing.TYPE_CHECKING:
    _CommandT = typing.TypeVar("_CommandT", bound=tanjun_abc.MessageCommand[typing.Any])
    _ParameterT = typing.TypeVar("_ParameterT", bound="Parameter")
    _ShlexParserT = typing.TypeVar("_ShlexParserT", bound="ShlexParser")
    _T_contra = typing.TypeVar("_T_contra", contravariant=True)
    _OtherT = typing.TypeVar("_OtherT")

    class _CmpProto(typing.Protocol[_T_contra]):
        def __gt__(self, __other: _T_contra) -> bool:
            raise NotImplementedError

        def __lt__(self, __other: _T_contra) -> bool:
            raise NotImplementedError

    _CmpProtoT = typing.TypeVar("_CmpProtoT", bound=_CmpProto[typing.Any])

_T = typing.TypeVar("_T")

ConverterSig = collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]]
"""Type hint of a converter used within a parser instance.

This must be a callable or asynchronous callable which takes one position
`str`, argument and returns the resultant value.
"""

_MaybeIterable = typing.Union[collections.Iterable[_T], _T]


class UndefinedT:
    """Singleton used to indicate an undefined value within parsing logic."""

    __singleton: typing.Optional[UndefinedT] = None

    def __new__(cls) -> UndefinedT:
        if cls.__singleton is None:
            cls.__singleton = super().__new__(cls)
            assert isinstance(cls.__singleton, UndefinedT)

        return cls.__singleton

    def __repr__(self) -> str:
        return "UNDEFINED"

    def __bool__(self) -> typing.Literal[False]:
        return False


UndefinedDefaultT = UndefinedT
"""Deprecated alias of `UndefinedT`."""

UNDEFINED = UndefinedT()
"""A singleton used to represent an undefined value within parsing logic."""

UNDEFINED_DEFAULT = UNDEFINED
"""Deprecated alias of `UNDEFINED`."""

_UndefinedOr = typing.Union[UndefinedT, _T]


class AbstractOptionParser(tanjun_abc.MessageParser, abc.ABC):
    """Abstract interface of a message content parser."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def arguments(self) -> collections.Sequence[Argument]:
        """Sequence of the positional arguments registered with this parser."""

    @property
    @abc.abstractmethod
    def options(self) -> collections.Sequence[Option]:
        """Sequence of the named options registered with this parser."""

    @typing.overload
    @abc.abstractmethod
    def add_argument(
        self: _T,
        key: str,
        /,
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @typing.overload
    @abc.abstractmethod
    def add_argument(
        self: _T,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @typing.overload
    @abc.abstractmethod
    def add_argument(
        self: _T,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[_OtherT]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @typing.overload
    @abc.abstractmethod
    def add_argument(
        self: _T,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        multi: bool = False,
    ) -> _T:
        ...

    @abc.abstractmethod
    def add_argument(
        self: _T,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        """Add a positional argument type to the parser..

        .. note::
            Order matters for positional arguments.

        Parameters
        ----------
        key : str
            The string identifier of this argument (may be used to pass the result
            of this argument to the command's callback during execution).

        Other Parameters
        ----------------
        converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
            The converter(s) this argument should use to handle values passed to it
            during parsing.

            If no converters are provided then the raw string value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The default value of this argument, if left as
            `UNDEFINED` then this will have no default.
        greedy : bool
            Whether or not this argument should be greedy (meaning that it
            takes in the remaining argument values).
        max_value
            Assert that the parsed value(s) for this argument are less than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        min_value
            Assert that the parsed value(s) for this argument are greater than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        multi : bool
            Whether this argument can be passed multiple times.

        Returns
        -------
        Self
            This parser to enable chained calls.
        """

    @typing.overload
    @abc.abstractmethod
    def add_option(
        self: _T,
        key: str,
        name: str,
        /,
        *names: str,
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @typing.overload
    @abc.abstractmethod
    def add_option(
        self: _T,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @typing.overload
    @abc.abstractmethod
    def add_option(
        self: _T,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[_OtherT]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @typing.overload
    @abc.abstractmethod
    def add_option(
        self: _T,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        ...

    @abc.abstractmethod
    def add_option(
        self: _T,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _T:
        """Add an named option to this parser.

        Parameters
        ----------
        key : str
            The string identifier of this option which will be used to pass the
            result of this option to the command's callback during execution as
            a keyword argument.
        name : str
            The name of this option used for identifying it in the parsed content.
        default : typing.Any
            The default value of this option, unlike arguments this is required
            for options.

        Other Parameters
        ----------------
        *names : str
            Other names of this option used for identifying it in the parsed content.
        converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
            The converter(s) this option should use to handle values passed to it
            during parsing.

            If no converters are provided then the raw string value will be passed.

            Only the first converter to pass will be used.
        empty_value : typing.Any
            The value to use if this option is provided without a value.
            If left as `UNDEFINED` then this option will error if it's
            provided without a value.
        max_value
            Assert that the parsed value(s) for this option are less than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        min_value
            Assert that the parsed value(s) for this option are greater than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        multi : bool
            If this option can be provided multiple times.
            Defaults to `False`.

        Returns
        -------
        Self
            This parser to enable chained calls.
        """


AbstractParser = AbstractOptionParser
"""Deprecated alias of `AbstractOptionParser`."""


class _ShlexTokenizer:
    __slots__ = ("__arg_buffer", "__last_name", "__options_buffer", "__shlex")

    def __init__(self, content: str, /) -> None:
        self.__arg_buffer: list[str] = []
        self.__last_name: typing.Optional[str] = None
        self.__options_buffer: list[tuple[str, typing.Optional[str]]] = []
        self.__shlex = shlex.shlex(content, posix=True)
        self.__shlex.commenters = ""
        self.__shlex.quotes = '"'
        self.__shlex.whitespace = " "
        self.__shlex.whitespace_split = True

    def collect_raw_options(self) -> collections.Mapping[str, collections.Sequence[typing.Optional[str]]]:
        results: dict[str, list[typing.Optional[str]]] = {}

        while (option := self.next_raw_option()) is not None:
            name, value = option

            if name not in results:
                results[name] = []

            results[name].append(value)

        return results

    def iter_raw_arguments(self) -> collections.Iterator[str]:
        while (argument := self.next_raw_argument()) is not None:
            yield argument

    def next_raw_argument(self) -> typing.Optional[str]:
        if self.__arg_buffer:
            return self.__arg_buffer.pop(0)

        while (value := self.__seek_shlex()) and value[0] == 1:
            self.__options_buffer.append(value[1])

        return value[1] if value else None

    def next_raw_option(self) -> typing.Optional[tuple[str, typing.Optional[str]]]:
        if self.__options_buffer:
            return self.__options_buffer.pop(0)

        while (value := self.__seek_shlex()) and value[0] == 0:
            self.__arg_buffer.append(value[1])

        return value[1] if value else None

    def __seek_shlex(
        self,
    ) -> typing.Union[tuple[typing.Literal[0], str], tuple[typing.Literal[1], tuple[str, typing.Optional[str]]], None]:
        option_name = self.__last_name

        try:
            value = next(self.__shlex)

        except StopIteration:
            if option_name is not None:
                self.__last_name = None
                return (1, (option_name, None))

            return None

        except ValueError as exc:
            raise errors.ParserError(str(exc), None) from exc

        is_option = value.startswith("-")
        if is_option and option_name is not None:
            self.__last_name = value
            return (1, (option_name, None))

        if is_option:
            self.__last_name = value
            return self.__seek_shlex()

        if option_name:
            self.__last_name = None
            return (1, (option_name, value))

        return (0, value)


async def _covert_option_or_empty(
    ctx: tanjun_abc.MessageContext, option: Option, value: typing.Optional[typing.Any], /
) -> typing.Any:
    if value is not None:
        return await option.convert(ctx, value)

    if option.empty_value is not UNDEFINED:
        return option.empty_value

    raise errors.NotEnoughArgumentsError(f"Option '{option.key} cannot be empty.", option.key)


class _SemanticShlex(_ShlexTokenizer):
    __slots__ = ("__arguments", "__ctx", "__options")

    def __init__(
        self,
        ctx: tanjun_abc.MessageContext,
        arguments: collections.Sequence[Argument],
        options: collections.Sequence[Option],
        /,
    ) -> None:
        super().__init__(ctx.content)
        self.__arguments = arguments
        self.__ctx = ctx
        self.__options = options

    async def parse(self) -> dict[str, typing.Any]:
        raw_options = self.collect_raw_options()
        results = asyncio.gather(*map(lambda option: self.__process_option(option, raw_options), self.__options))
        values = dict(zip((option.key for option in self.__options), await results))

        for argument in self.__arguments:
            values[argument.key] = await self.__process_argument(argument)

            if argument.is_greedy or argument.is_multi:
                break  # Multi and Greedy parameters should always be the last parameter.

        return values

    async def __process_argument(self, argument: Argument) -> typing.Any:
        if argument.is_greedy and (value := " ".join(self.iter_raw_arguments())):
            return await argument.convert(self.__ctx, value)

        if argument.is_multi and (values := list(self.iter_raw_arguments())):
            return await asyncio.gather(*(argument.convert(self.__ctx, value) for value in values))

        # If the previous two statements failed on getting raw arguments then this will as well.
        if (optional_value := self.next_raw_argument()) is not None:
            return await argument.convert(self.__ctx, optional_value)

        if argument.default is not UNDEFINED:
            return argument.default

        # If this is reached then no value was found.
        raise errors.NotEnoughArgumentsError(f"Missing value for required argument '{argument.key}'", argument.key)

    async def __process_option(
        self, option: Option, raw_options: collections.Mapping[str, collections.Sequence[typing.Optional[str]]]
    ) -> typing.Any:
        values_iter = itertools.chain.from_iterable(raw_options[name] for name in option.names if name in raw_options)
        if option.is_multi and (values := list(values_iter)):
            return await asyncio.gather(*(_covert_option_or_empty(self.__ctx, option, value) for value in values))

        if not option.is_multi and (value := next(values_iter, ...)) is not ...:
            if next(values_iter, ...) is not ...:
                raise errors.TooManyArgumentsError(f"Option `{option.key}` can only take a single value", option.key)

            return await _covert_option_or_empty(self.__ctx, option, value)

        if option.default is not UNDEFINED:
            return option.default

        # If this is reached then no value was found.
        raise errors.NotEnoughArgumentsError(f"Missing required option `{option.key}`", option.key)


def _get_or_set_parser(command: tanjun_abc.MessageCommand[typing.Any], /) -> AbstractOptionParser:
    if not command.parser:
        parser = ShlexParser()
        command.set_parser(parser)
        return parser

    if isinstance(command.parser, AbstractOptionParser):
        return command.parser

    raise TypeError("Expected parser to be an instance of tanjun.parsing.AbstractOptionParser")


@typing.overload
def with_argument(
    key: str,
    /,
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    greedy: bool = False,
    max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    greedy: bool = False,
    max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[_T]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    greedy: bool = False,
    max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    greedy: bool = False,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


def with_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    greedy: bool = False,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add an argument to a message command through a decorator call.

    Notes
    -----
    * Order matters for positional arguments and since decorator execution
      starts at the decorator closest to the command and goes upwards this
      will decide where a positional argument is located in a command's
      signature.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this argument (may be used to pass the result
        of this argument to the command's callback during execution).
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    default : typing.Any
        The default value of this argument, if left as
        `UNDEFINED` then this will have no default.
    greedy : bool
        Whether or not this argument should be greedy (meaning that it
        takes in the remaining argument values).
    max_value
        Assert that the parsed value(s) for this argument are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this argument are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    multi : bool
        Whether this argument can be passed multiple times.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this argument is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_argument("command", converters=int, default=42)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: int):
        ...
    ```
    """

    def decorator(command: _CommandT, /) -> _CommandT:
        _get_or_set_parser(command).add_argument(
            key,
            converters=converters,
            default=default,
            greedy=greedy,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )
        return command

    return decorator


@typing.overload
def with_greedy_argument(
    key: str,
    /,
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_greedy_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_greedy_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[_T]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_greedy_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


def with_greedy_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add a greedy argument to a message command through a decorator call.

    Notes
    -----
    * A greedy argument will consume the remaining positional arguments and pass
      them through to the converters as one joined string while also requiring
      that at least one more positional argument is remaining unless a
      default is set.
    * Order matters for positional arguments and since decorator execution
      starts at the decorator closest to the command and goes upwards this
      will decide where a positional argument is located in a command's
      signature.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this argument (may be used to pass the result
        of this argument to the command's callback during execution).

    Other Parameters
    ----------------
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    default : typing.Any
        The default value of this argument, if left as
        `UNDEFINED` then this will have no default.
    max_value
        Assert that the parsed value(s) for this argument are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this argument are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this argument is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_greedy_argument("command", converters=StringView)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: StringView):
        ...
    ```
    """
    return with_argument(
        key, converters=converters, default=default, greedy=True, max_value=max_value, min_value=min_value
    )


@typing.overload
def with_multi_argument(
    key: str,
    /,
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_multi_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_multi_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[_T]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_multi_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]],
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


def with_multi_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add a multi-argument to a message command through a decorator call.

    Notes
    -----
    * A multi argument will consume the remaining positional arguments and pass
      them to the converters through multiple calls while also requiring that
      at least one more positional argument is remaining unless a default is
      set and passing through the results to the command's callback as a
      sequence.
    * Order matters for positional arguments and since decorator execution
      starts at the decorator closest to the command and goes upwards this
      will decide where a positional argument is located in a command's
      signature.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this argument (may be used to pass the result
        of this argument to the command's callback during execution).

    Other Parameters
    ----------------
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    default : typing.Any
        The default value of this argument, if left as
        `UNDEFINED` then this will have no default.
    max_value
        Assert that the parsed value(s) for this argument are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this argument are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this argument is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_multi_argument("command", converters=int)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
        ...
    ```
    """
    return with_argument(
        key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=True
    )


@typing.overload
def with_option(
    key: str,
    name: str,
    /,
    *names: str,
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[_T]],
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[typing.Any]],
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


# TODO: add default getter
def with_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add an option to a message command through a decorator call.

    .. note::
        If no parser is explicitly set on the command this is decorating before
        this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this option which will be used to pass the
        result of this argument to the command's callback during execution as
        a keyword argument.
    name : str
        The name of this option used for identifying it in the parsed content.
    default : typing.Any
        The default value of this argument, unlike arguments this is required
        for options.

    Other Parameters
    ----------------
    *names : str
        Other names of this option used for identifying it in the parsed content.
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    empty_value : typing.Any
        The value to use if this option is provided without a value. If left as
        `UNDEFINED` then this option will error if it's
        provided without a value.
    max_value
        Assert that the parsed value(s) for this option are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this option are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    multi : bool
        If this option can be provided multiple times.
        Defaults to `False`.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this option is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_option("command", converters=int, default=42)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: int):
        ...
    ```
    """

    def decorator(command: _CommandT, /) -> _CommandT:
        _get_or_set_parser(command).add_option(
            key,
            name,
            *names,
            converters=converters,
            default=default,
            empty_value=empty_value,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )
        return command

    return decorator


@typing.overload
def with_multi_option(
    key: str,
    name: str,
    /,
    *names: str,
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_multi_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_multi_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[_T]],
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


@typing.overload
def with_multi_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[typing.Any]],
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    ...


def with_multi_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add an multi-option to a command's parser through a decorator call.

    Notes
    -----
    * A multi option will consume all the values provided for an option and
      pass them through to the converters as an array of strings while also
      requiring that at least one value is provided for the option unless
      a default is set.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this option which will be used to pass the
        result of this argument to the command's callback during execution as
        a keyword argument.
    name : str
        The name of this option used for identifying it in the parsed content.
    default : typing.Any
        The default value of this argument, unlike arguments this is required
        for options.

    Other Parameters
    ----------------
    *names : str
        Other names of this option used for identifying it in the parsed content.
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    empty_value : typing.Any
        The value to use if this option is provided without a value. If left as
        `UNDEFINED` then this option will error if it's
        provided without a value.
    max_value
        Assert that the parsed value(s) for this option are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this option are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this option is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_multi_option("command", converters=int, default=())
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
        ...
    ```
    """
    return with_option(
        key,
        name,
        *names,
        converters=converters,
        default=default,
        empty_value=empty_value,
        max_value=max_value,
        min_value=min_value,
        multi=True,
    )


class Parameter:
    """Base class for parameters for the standard parser(s)."""

    __slots__ = ("_client", "_component", "_converters", "_default", "_is_multi", "_key", "_max_value", "_min_value")

    def __init__(
        self,
        key: str,
        /,
        *,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> None:
        """Initialise a parameter."""
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._component: typing.Optional[tanjun_abc.Component] = None
        self._converters: list[injecting.CallbackDescriptor[typing.Any]] = []
        self._default = default
        self._is_multi = multi
        self._key = key
        self._max_value = max_value
        self._min_value = min_value

        if key.startswith("-"):
            raise ValueError("parameter key cannot start with `-`")

        if isinstance(converters, collections.Iterable):
            for converter in converters:
                self._add_converter(converter)

        else:
            self._add_converter(converters)

    def __repr__(self) -> str:
        return f"{type(self).__name__} <{self._key}>"

    @property
    def converters(self) -> collections.Sequence[ConverterSig[typing.Any]]:
        """Sequence of the converters registered for this parameter."""
        return tuple(converter.callback for converter in self._converters)

    @property
    def default(self) -> _UndefinedOr[typing.Any]:
        """The parameter's default.

        If this is `UndefinedT` then this parameter is required.
        """
        return self._default

    @property
    def is_multi(self) -> bool:
        """Whether this parameter is "multi".

        Multi parameters will be passed a list of all the values provided for
        this parameter (with each entry being converted separately.)
        """
        return self._is_multi

    @property
    def key(self) -> str:
        """The key of this parameter used to pass the result to the command's callback."""
        return self._key

    @property
    def needs_injector(self) -> bool:
        """Whether this parameter needs an injector to be used."""
        # TODO: cache this value?
        return any(converter.needs_injector for converter in self._converters)

    def _add_converter(self, converter: ConverterSig[typing.Any], /) -> None:
        if isinstance(converter, conversion.BaseConverter):
            if self._client:
                converter.check_client(self._client, f"{self._key} parameter")

        if not isinstance(converter, injecting.CallbackDescriptor):
            # Some types like `bool` and `bytes` are overridden here for the sake of convenience.
            converter = conversion.override_type(converter)
            converter_ = injecting.CallbackDescriptor(converter)
            self._converters.append(converter_)

        else:
            self._converters.append(converter)

    def bind_client(self, client: tanjun_abc.Client, /) -> None:
        self._client = client
        for converter in self._converters:
            if isinstance(converter.callback, conversion.BaseConverter):
                converter.callback.check_client(client, f"{self._key} parameter")

    def bind_component(self, component: tanjun_abc.Component, /) -> None:
        self._component = component

    def _validate(self, value: typing.Any, /) -> None:
        # assert value >= self._min_value
        if self._min_value is not UNDEFINED and self._min_value > value:
            raise errors.ConversionError(
                f"{self._key!r} must be greater than or equal to {self._min_value!r}", self.key
            )

        # assert value <= self._max_value
        if self._max_value is not UNDEFINED and self._max_value < value:
            raise errors.ConversionError(f"{self._key!r} must be less than or equal to {self._max_value!r}", self.key)

    async def convert(self, ctx: tanjun_abc.Context, value: str) -> typing.Any:
        """Convert the given value to the type of this parameter."""
        if not self._converters:
            self._validate(value)
            return value

        sources: list[ValueError] = []
        for converter in self._converters:
            try:
                result = await converter.resolve_with_command_context(ctx, value)

            except ValueError as exc:
                sources.append(exc)

            else:
                self._validate(result)
                return result

        parameter_type = "option" if isinstance(self, Option) else "argument"
        raise errors.ConversionError(f"Couldn't convert {parameter_type} '{self.key}'", self.key, sources)

    def copy(self: _ParameterT, *, _new: bool = True) -> _ParameterT:
        """Copy the parameter.

        Returns
        -------
        Self
            A copy of the parameter.
        """
        if not _new:
            self._converters = [converter.copy() for converter in self._converters]
            return self

        result = copy.copy(self).copy(_new=False)
        return result


class Argument(Parameter):
    """Representation of a positional argument used by the standard parser."""

    __slots__ = ("_is_greedy",)

    def __init__(
        self,
        key: str,
        /,
        *,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> None:
        """Initialise a positional argument.

        Parameters
        ----------
        key : str
            The string identifier of this argument (may be used to pass the result
            of this argument to the command's callback during execution).

        Other Parameters
        ----------------
        converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
            The converter(s) this argument should use to handle values passed to it
            during parsing.

            If no converters are provided then the raw string value will be passed.

            Only the first converter to pass will be used.
        default : typing.Any
            The default value of this argument, if left as
            `UNDEFINED` then this will have no default.
        greedy : bool
            Whether or not this argument should be greedy (meaning that it
            takes in the remaining argument values).
        max_value
            Assert that the parsed value(s) for this option are less than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        min_value
            Assert that the parsed value(s) for this option are greater than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        multi : bool
            Whether this argument can be passed multiple times.
        """
        if greedy and multi:
            raise ValueError("Argument cannot be both greed and multi.")

        self._is_greedy = greedy
        super().__init__(
            key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=multi
        )

    @property
    def is_greedy(self) -> bool:
        """Whether this parameter is greedy.

        Greedy parameters will consume the remaining message content as one
        string (with converters also being passed the whole string).

        .. note::
            Greedy and multi parameters cannot be used together.
        """
        return self._is_greedy


class Option(Parameter):
    """Representation of a named optional parameter used by the standard parser."""

    __slots__ = ("_empty_value", "_names")

    def __init__(
        self,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = True,
    ) -> None:
        """Initialise a named optional parameter.

        Parameters
        ----------
        key : str
            The string identifier of this option which will be used to pass the
            result of this argument to the command's callback during execution as
            a keyword argument.
        name : str
            The name of this option used for identifying it in the parsed content.
        default : typing.Any
            The default value of this argument, unlike arguments this is required
            for options.

        Other Parameters
        ----------------
        *names : str
            Other names of this option used for identifying it in the parsed content.
        converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
            The converter(s) this argument should use to handle values passed to it
            during parsing.

            If no converters are provided then the raw string value will be passed.

            Only the first converter to pass will be used.
        empty_value : typing.Any
            The value to use if this option is provided without a value. If left as
            `UNDEFINED` then this option will error if it's
            provided without a value.
        max_value
            Assert that the parsed value(s) for this option are less than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        min_value
            Assert that the parsed value(s) for this option are greater than or equal to this.

            If any converters are provided then this should be compatible
            with the result of them.
        multi : bool
            If this option can be provided multiple times.
            Defaults to `False`.
        """
        if not name.startswith("-") or not all(n.startswith("-") for n in names):
            raise ValueError("All option names must start with `-`")

        self._empty_value = empty_value
        self._names = [name, *names]
        super().__init__(
            key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=multi
        )

    @property
    def empty_value(self) -> _UndefinedOr[typing.Any]:
        """The value to return if the option is empty.

        If this is `UndefinedT` then a value will be required for the
        option.
        """
        return self._empty_value

    @property
    def names(self) -> collections.Sequence[str]:
        """Sequence of the CLI names of this option."""
        return self._names.copy()

    def __repr__(self) -> str:
        return f"{type(self).__name__} <{self.key}, {self._names}>"


class ShlexParser(AbstractOptionParser):
    """A shlex based `AbstractOptionParser` implementation."""

    __slots__ = ("_arguments", "_client", "_component", "_options")

    def __init__(self) -> None:
        """Initialise a shlex parser."""
        self._arguments: list[Argument] = []
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._component: typing.Optional[tanjun_abc.Component] = None
        self._options: list[Option] = []  # TODO: maybe switch to dict[str, Option] and assert doesn't already exist

    @property
    def needs_injector(self) -> bool:
        """Whether this parser needs an injector to be used."""
        # TODO: cache this value?
        return any(parameter.needs_injector for parameter in itertools.chain(self._options, self._arguments))

    @property
    def arguments(self) -> collections.Sequence[Argument]:
        # <<inherited docstring from AbstractOptionParser>>.
        return self._arguments.copy()

    @property
    def options(self) -> collections.Sequence[Option]:
        # <<inherited docstring from AbstractOptionParser>>.
        return self._options.copy()

    def copy(self: _ShlexParserT, *, _new: bool = True) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        if not _new:
            self._arguments = [argument.copy() for argument in self._arguments]
            self._options = [option.copy() for option in self._options]
            return self

        return copy.copy(self).copy(_new=False)

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[_T]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        argument = Argument(
            key,
            converters=converters,
            default=default,
            greedy=greedy,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )

        if self._client:
            argument.bind_client(self._client)

        if self._component:
            argument.bind_component(self._component)

        found_final_argument = False

        for argument in self._arguments:
            if found_final_argument:
                del self._arguments[-1]
                raise ValueError("Multi or greedy argument must be the last argument")

            found_final_argument = argument.is_multi or argument.is_greedy

        self._arguments.append(argument)
        return self

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[_T]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    # TODO: add default getter
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        option = Option(
            key,
            name,
            *names,
            converters=converters,
            default=default,
            empty_value=empty_value,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )

        if self._client:
            option.bind_client(self._client)

        if self._component:
            option.bind_component(self._component)

        self._options.append(option)
        return self

    def bind_client(self: _ShlexParserT, client: tanjun_abc.Client, /) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        self._client = client
        for parameter in itertools.chain(self._options, self._arguments):
            parameter.bind_client(client)

        return self

    def bind_component(self: _ShlexParserT, component: tanjun_abc.Component, /) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        self._component = component
        for parameter in itertools.chain(self._options, self._arguments):
            parameter.bind_component(component)

        return self

    def parse(
        self, ctx: tanjun_abc.MessageContext, /
    ) -> collections.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]:
        # <<inherited docstring from AbstractOptionParser>>.
        return _SemanticShlex(ctx, self._arguments, self._options).parse()


def with_parser(command: _CommandT, /) -> _CommandT:
    """Add a shlex parser command parser to a supported command.

    Example
    -------
    ```py
    @tanjun.with_argument("arg", converters=int)
    @tanjun.with_parser
    @tanjun.as_message_command("hi")
    async def hi(ctx: tanjun.MessageContext, arg: int) -> None:
        ...
    ```

    Parameters
    ----------
    command : tanjun.abc.MessageCommands
        The message command to set the parser on.

    Returns
    -------
    tanjun.abc.MessageCommand
        The command with the parser set.

    Raises
    ------
    ValueError
        If the command already has a parser set.
    """
    if command.parser:
        raise ValueError("Command already has a parser set")

    return command.set_parser(ShlexParser())

Standard implementation of message command argument parsing.

View Source
class ShlexParser(AbstractOptionParser):
    """A shlex based `AbstractOptionParser` implementation."""

    __slots__ = ("_arguments", "_client", "_component", "_options")

    def __init__(self) -> None:
        """Initialise a shlex parser."""
        self._arguments: list[Argument] = []
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._component: typing.Optional[tanjun_abc.Component] = None
        self._options: list[Option] = []  # TODO: maybe switch to dict[str, Option] and assert doesn't already exist

    @property
    def needs_injector(self) -> bool:
        """Whether this parser needs an injector to be used."""
        # TODO: cache this value?
        return any(parameter.needs_injector for parameter in itertools.chain(self._options, self._arguments))

    @property
    def arguments(self) -> collections.Sequence[Argument]:
        # <<inherited docstring from AbstractOptionParser>>.
        return self._arguments.copy()

    @property
    def options(self) -> collections.Sequence[Option]:
        # <<inherited docstring from AbstractOptionParser>>.
        return self._options.copy()

    def copy(self: _ShlexParserT, *, _new: bool = True) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        if not _new:
            self._arguments = [argument.copy() for argument in self._arguments]
            self._options = [option.copy() for option in self._options]
            return self

        return copy.copy(self).copy(_new=False)

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[_T]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]],
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        argument = Argument(
            key,
            converters=converters,
            default=default,
            greedy=greedy,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )

        if self._client:
            argument.bind_client(self._client)

        if self._component:
            argument.bind_component(self._component)

        found_final_argument = False

        for argument in self._arguments:
            if found_final_argument:
                del self._arguments[-1]
                raise ValueError("Multi or greedy argument must be the last argument")

            found_final_argument = argument.is_multi or argument.is_greedy

        self._arguments.append(argument)
        return self

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[_CmpProtoT]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[_T]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    @typing.overload
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]],
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        ...

    # TODO: add default getter
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        option = Option(
            key,
            name,
            *names,
            converters=converters,
            default=default,
            empty_value=empty_value,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )

        if self._client:
            option.bind_client(self._client)

        if self._component:
            option.bind_component(self._component)

        self._options.append(option)
        return self

    def bind_client(self: _ShlexParserT, client: tanjun_abc.Client, /) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        self._client = client
        for parameter in itertools.chain(self._options, self._arguments):
            parameter.bind_client(client)

        return self

    def bind_component(self: _ShlexParserT, component: tanjun_abc.Component, /) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        self._component = component
        for parameter in itertools.chain(self._options, self._arguments):
            parameter.bind_component(component)

        return self

    def parse(
        self, ctx: tanjun_abc.MessageContext, /
    ) -> collections.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]:
        # <<inherited docstring from AbstractOptionParser>>.
        return _SemanticShlex(ctx, self._arguments, self._options).parse()

A shlex based AbstractOptionParser implementation.

#   ShlexParser()
View Source
    def __init__(self) -> None:
        """Initialise a shlex parser."""
        self._arguments: list[Argument] = []
        self._client: typing.Optional[tanjun_abc.Client] = None
        self._component: typing.Optional[tanjun_abc.Component] = None
        self._options: list[Option] = []  # TODO: maybe switch to dict[str, Option] and assert doesn't already exist

Initialise a shlex parser.

#   needs_injector: bool

Whether this parser needs an injector to be used.

#   arguments: collections.abc.Sequence[tanjun.parsing.Argument]

Sequence of the positional arguments registered with this parser.

#   options: collections.abc.Sequence[tanjun.parsing.Option]

Sequence of the named options registered with this parser.

#   def copy(self: ~_ShlexParserT, *, _new: bool = True) -> ~_ShlexParserT:
View Source
    def copy(self: _ShlexParserT, *, _new: bool = True) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        if not _new:
            self._arguments = [argument.copy() for argument in self._arguments]
            self._options = [option.copy() for option in self._options]
            return self

        return copy.copy(self).copy(_new=False)

Copy the parser.

Returns
  • Self: A copy of the parser.
#   def add_argument( self: ~_ShlexParserT, key: str, /, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), *, default: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, greedy: bool = False, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, multi: bool = False ) -> ~_ShlexParserT:
View Source
    def add_argument(
        self: _ShlexParserT,
        key: str,
        /,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        *,
        default: _UndefinedOr[typing.Any] = UNDEFINED,
        greedy: bool = False,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        argument = Argument(
            key,
            converters=converters,
            default=default,
            greedy=greedy,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )

        if self._client:
            argument.bind_client(self._client)

        if self._component:
            argument.bind_component(self._component)

        found_final_argument = False

        for argument in self._arguments:
            if found_final_argument:
                del self._arguments[-1]
                raise ValueError("Multi or greedy argument must be the last argument")

            found_final_argument = argument.is_multi or argument.is_greedy

        self._arguments.append(argument)
        return self

Add a positional argument type to the parser..

Note: Order matters for positional arguments.

Parameters
  • key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
Other Parameters
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The default value of this argument, if left as UNDEFINED then this will have no default.
  • greedy (bool): Whether or not this argument should be greedy (meaning that it takes in the remaining argument values).
  • max_value: Assert that the parsed value(s) for this argument are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • multi (bool): Whether this argument can be passed multiple times.
Returns
  • Self: This parser to enable chained calls.
#   def add_option( self: ~_ShlexParserT, key: str, name: str, /, *names: str, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any, empty_value: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, multi: bool = False ) -> ~_ShlexParserT:
View Source
    def add_option(
        self: _ShlexParserT,
        key: str,
        name: str,
        /,
        *names: str,
        converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
        default: typing.Any,
        empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
        max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
        multi: bool = False,
    ) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        option = Option(
            key,
            name,
            *names,
            converters=converters,
            default=default,
            empty_value=empty_value,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )

        if self._client:
            option.bind_client(self._client)

        if self._component:
            option.bind_component(self._component)

        self._options.append(option)
        return self

Add an named option to this parser.

Parameters
  • key (str): The string identifier of this option which will be used to pass the result of this option to the command's callback during execution as a keyword argument.
  • name (str): The name of this option used for identifying it in the parsed content.
  • default (typing.Any): The default value of this option, unlike arguments this is required for options.
Other Parameters
  • *names (str): Other names of this option used for identifying it in the parsed content.
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this option should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • empty_value (typing.Any): The value to use if this option is provided without a value. If left as UNDEFINED then this option will error if it's provided without a value.
  • max_value: Assert that the parsed value(s) for this option are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this option are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • multi (bool): If this option can be provided multiple times. Defaults to False.
Returns
  • Self: This parser to enable chained calls.
#   def bind_client(self: ~_ShlexParserT, client: tanjun.abc.Client, /) -> ~_ShlexParserT:
View Source
    def bind_client(self: _ShlexParserT, client: tanjun_abc.Client, /) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        self._client = client
        for parameter in itertools.chain(self._options, self._arguments):
            parameter.bind_client(client)

        return self
#   def bind_component( self: ~_ShlexParserT, component: tanjun.abc.Component, / ) -> ~_ShlexParserT:
View Source
    def bind_component(self: _ShlexParserT, component: tanjun_abc.Component, /) -> _ShlexParserT:
        # <<inherited docstring from AbstractOptionParser>>.
        self._component = component
        for parameter in itertools.chain(self._options, self._arguments):
            parameter.bind_component(component)

        return self
#   def parse( self, ctx: tanjun.abc.MessageContext, / ) -> collections.abc.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]:
View Source
    def parse(
        self, ctx: tanjun_abc.MessageContext, /
    ) -> collections.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]:
        # <<inherited docstring from AbstractOptionParser>>.
        return _SemanticShlex(ctx, self._arguments, self._options).parse()

Parse a message context.

Warning: This relies on the prefix and command name(s) having been removed from tanjun.abc.MessageContext.content

Parameters
Returns
  • dict[str, typing.Any]: Dictionary of argument names to the parsed values for them.
Raises
#   def with_argument( key: str, /, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), *, default: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, greedy: bool = False, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, multi: bool = False ) -> collections.abc.Callable[[~_CommandT], ~_CommandT]:
View Source
def with_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    greedy: bool = False,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add an argument to a message command through a decorator call.

    Notes
    -----
    * Order matters for positional arguments and since decorator execution
      starts at the decorator closest to the command and goes upwards this
      will decide where a positional argument is located in a command's
      signature.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this argument (may be used to pass the result
        of this argument to the command's callback during execution).
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    default : typing.Any
        The default value of this argument, if left as
        `UNDEFINED` then this will have no default.
    greedy : bool
        Whether or not this argument should be greedy (meaning that it
        takes in the remaining argument values).
    max_value
        Assert that the parsed value(s) for this argument are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this argument are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    multi : bool
        Whether this argument can be passed multiple times.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this argument is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_argument("command", converters=int, default=42)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: int):
        ...
    ```
    """

    def decorator(command: _CommandT, /) -> _CommandT:
        _get_or_set_parser(command).add_argument(
            key,
            converters=converters,
            default=default,
            greedy=greedy,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )
        return command

    return decorator

Add an argument to a message command through a decorator call.

Notes
  • Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature.
  • If no parser is explicitly set on the command this is decorating before this decorator call then this will set ShlexParser as the parser.
Parameters
  • key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The default value of this argument, if left as UNDEFINED then this will have no default.
  • greedy (bool): Whether or not this argument should be greedy (meaning that it takes in the remaining argument values).
  • max_value: Assert that the parsed value(s) for this argument are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • multi (bool): Whether this argument can be passed multiple times.
Returns
Examples
import tanjun

@tanjun.parsing.with_argument("command", converters=int, default=42)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: int):
    ...
#   def with_greedy_argument( key: str, /, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), *, default: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED ) -> collections.abc.Callable[[~_CommandT], ~_CommandT]:
View Source
def with_greedy_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add a greedy argument to a message command through a decorator call.

    Notes
    -----
    * A greedy argument will consume the remaining positional arguments and pass
      them through to the converters as one joined string while also requiring
      that at least one more positional argument is remaining unless a
      default is set.
    * Order matters for positional arguments and since decorator execution
      starts at the decorator closest to the command and goes upwards this
      will decide where a positional argument is located in a command's
      signature.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this argument (may be used to pass the result
        of this argument to the command's callback during execution).

    Other Parameters
    ----------------
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    default : typing.Any
        The default value of this argument, if left as
        `UNDEFINED` then this will have no default.
    max_value
        Assert that the parsed value(s) for this argument are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this argument are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this argument is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_greedy_argument("command", converters=StringView)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: StringView):
        ...
    ```
    """
    return with_argument(
        key, converters=converters, default=default, greedy=True, max_value=max_value, min_value=min_value
    )

Add a greedy argument to a message command through a decorator call.

Notes
  • A greedy argument will consume the remaining positional arguments and pass them through to the converters as one joined string while also requiring that at least one more positional argument is remaining unless a default is set.
  • Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature.
  • If no parser is explicitly set on the command this is decorating before this decorator call then this will set ShlexParser as the parser.
Parameters
  • key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
Other Parameters
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The default value of this argument, if left as UNDEFINED then this will have no default.
  • max_value: Assert that the parsed value(s) for this argument are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

Returns
Examples
import tanjun

@tanjun.parsing.with_greedy_argument("command", converters=StringView)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: StringView):
    ...
#   def with_multi_argument( key: str, /, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), *, default: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED ) -> collections.abc.Callable[[~_CommandT], ~_CommandT]:
View Source
def with_multi_argument(
    key: str,
    /,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    *,
    default: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add a multi-argument to a message command through a decorator call.

    Notes
    -----
    * A multi argument will consume the remaining positional arguments and pass
      them to the converters through multiple calls while also requiring that
      at least one more positional argument is remaining unless a default is
      set and passing through the results to the command's callback as a
      sequence.
    * Order matters for positional arguments and since decorator execution
      starts at the decorator closest to the command and goes upwards this
      will decide where a positional argument is located in a command's
      signature.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this argument (may be used to pass the result
        of this argument to the command's callback during execution).

    Other Parameters
    ----------------
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    default : typing.Any
        The default value of this argument, if left as
        `UNDEFINED` then this will have no default.
    max_value
        Assert that the parsed value(s) for this argument are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this argument are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this argument is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_multi_argument("command", converters=int)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
        ...
    ```
    """
    return with_argument(
        key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=True
    )

Add a multi-argument to a message command through a decorator call.

Notes
  • A multi argument will consume the remaining positional arguments and pass them to the converters through multiple calls while also requiring that at least one more positional argument is remaining unless a default is set and passing through the results to the command's callback as a sequence.
  • Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature.
  • If no parser is explicitly set on the command this is decorating before this decorator call then this will set ShlexParser as the parser.
Parameters
  • key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
Other Parameters
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • default (typing.Any): The default value of this argument, if left as UNDEFINED then this will have no default.
  • max_value: Assert that the parsed value(s) for this argument are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

Returns
Examples
import tanjun

@tanjun.parsing.with_multi_argument("command", converters=int)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
    ...
#   def with_option( key: str, name: str, /, *names: str, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any, empty_value: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, multi: bool = False ) -> collections.abc.Callable[[~_CommandT], ~_CommandT]:
View Source
def with_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    multi: bool = False,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add an option to a message command through a decorator call.

    .. note::
        If no parser is explicitly set on the command this is decorating before
        this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this option which will be used to pass the
        result of this argument to the command's callback during execution as
        a keyword argument.
    name : str
        The name of this option used for identifying it in the parsed content.
    default : typing.Any
        The default value of this argument, unlike arguments this is required
        for options.

    Other Parameters
    ----------------
    *names : str
        Other names of this option used for identifying it in the parsed content.
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    empty_value : typing.Any
        The value to use if this option is provided without a value. If left as
        `UNDEFINED` then this option will error if it's
        provided without a value.
    max_value
        Assert that the parsed value(s) for this option are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this option are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    multi : bool
        If this option can be provided multiple times.
        Defaults to `False`.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this option is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_option("command", converters=int, default=42)
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: int):
        ...
    ```
    """

    def decorator(command: _CommandT, /) -> _CommandT:
        _get_or_set_parser(command).add_option(
            key,
            name,
            *names,
            converters=converters,
            default=default,
            empty_value=empty_value,
            max_value=max_value,
            min_value=min_value,
            multi=multi,
        )
        return command

    return decorator

Add an option to a message command through a decorator call.

Note: If no parser is explicitly set on the command this is decorating before this decorator call then this will set ShlexParser as the parser.

Parameters
  • key (str): The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument.
  • name (str): The name of this option used for identifying it in the parsed content.
  • default (typing.Any): The default value of this argument, unlike arguments this is required for options.
Other Parameters
  • *names (str): Other names of this option used for identifying it in the parsed content.
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • empty_value (typing.Any): The value to use if this option is provided without a value. If left as UNDEFINED then this option will error if it's provided without a value.
  • max_value: Assert that the parsed value(s) for this option are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this option are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • multi (bool): If this option can be provided multiple times. Defaults to False.
Returns
Examples
import tanjun

@tanjun.parsing.with_option("command", converters=int, default=42)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: int):
    ...
#   def with_multi_option( key: str, name: str, /, *names: str, converters: Union[collections.abc.Iterable[collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]], collections.abc.Callable[..., Union[Any, collections.abc.Awaitable[Any]]]] = (), default: Any, empty_value: Union[tanjun.parsing.UndefinedT, Any] = UNDEFINED, max_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED, min_value: Union[tanjun.parsing.UndefinedT, tanjun.parsing._CmpProto[Any]] = UNDEFINED ) -> collections.abc.Callable[[~_CommandT], ~_CommandT]:
View Source
def with_multi_option(
    key: str,
    name: str,
    /,
    *names: str,
    converters: _MaybeIterable[ConverterSig[typing.Any]] = (),
    default: typing.Any,
    empty_value: _UndefinedOr[typing.Any] = UNDEFINED,
    max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
    min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED,
) -> collections.Callable[[_CommandT], _CommandT]:
    """Add an multi-option to a command's parser through a decorator call.

    Notes
    -----
    * A multi option will consume all the values provided for an option and
      pass them through to the converters as an array of strings while also
      requiring that at least one value is provided for the option unless
      a default is set.
    * If no parser is explicitly set on the command this is decorating before
      this decorator call then this will set `ShlexParser` as the parser.

    Parameters
    ----------
    key : str
        The string identifier of this option which will be used to pass the
        result of this argument to the command's callback during execution as
        a keyword argument.
    name : str
        The name of this option used for identifying it in the parsed content.
    default : typing.Any
        The default value of this argument, unlike arguments this is required
        for options.

    Other Parameters
    ----------------
    *names : str
        Other names of this option used for identifying it in the parsed content.
    converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]
        The converter(s) this argument should use to handle values passed to it
        during parsing.

        If no converters are provided then the raw string value will be passed.

        Only the first converter to pass will be used.
    empty_value : typing.Any
        The value to use if this option is provided without a value. If left as
        `UNDEFINED` then this option will error if it's
        provided without a value.
    max_value
        Assert that the parsed value(s) for this option are less than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.
    min_value
        Assert that the parsed value(s) for this option are greater than or equal to this.

        If any converters are provided then this should be compatible
        with the result of them.

    Returns
    -------
    collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:
        Decorator function for the message command this option is being added to.

    Examples
    --------
    ```python
    import tanjun

    @tanjun.parsing.with_multi_option("command", converters=int, default=())
    @tanjun.parsing.with_parser
    @tanjun.component.as_message_command("command")
    async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
        ...
    ```
    """
    return with_option(
        key,
        name,
        *names,
        converters=converters,
        default=default,
        empty_value=empty_value,
        max_value=max_value,
        min_value=min_value,
        multi=True,
    )

Add an multi-option to a command's parser through a decorator call.

Notes
  • A multi option will consume all the values provided for an option and pass them through to the converters as an array of strings while also requiring that at least one value is provided for the option unless a default is set.
  • If no parser is explicitly set on the command this is decorating before this decorator call then this will set ShlexParser as the parser.
Parameters
  • key (str): The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument.
  • name (str): The name of this option used for identifying it in the parsed content.
  • default (typing.Any): The default value of this argument, unlike arguments this is required for options.
Other Parameters
  • *names (str): Other names of this option used for identifying it in the parsed content.
  • converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.

    If no converters are provided then the raw string value will be passed.

    Only the first converter to pass will be used.

  • empty_value (typing.Any): The value to use if this option is provided without a value. If left as UNDEFINED then this option will error if it's provided without a value.
  • max_value: Assert that the parsed value(s) for this option are less than or equal to this.

If any converters are provided then this should be compatible with the result of them.

  • min_value: Assert that the parsed value(s) for this option are greater than or equal to this.

If any converters are provided then this should be compatible with the result of them.

Returns
Examples
import tanjun

@tanjun.parsing.with_multi_option("command", converters=int, default=())
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
    ...
#   def with_parser(command: ~_CommandT, /) -> ~_CommandT:
View Source
def with_parser(command: _CommandT, /) -> _CommandT:
    """Add a shlex parser command parser to a supported command.

    Example
    -------
    ```py
    @tanjun.with_argument("arg", converters=int)
    @tanjun.with_parser
    @tanjun.as_message_command("hi")
    async def hi(ctx: tanjun.MessageContext, arg: int) -> None:
        ...
    ```

    Parameters
    ----------
    command : tanjun.abc.MessageCommands
        The message command to set the parser on.

    Returns
    -------
    tanjun.abc.MessageCommand
        The command with the parser set.

    Raises
    ------
    ValueError
        If the command already has a parser set.
    """
    if command.parser:
        raise ValueError("Command already has a parser set")

    return command.set_parser(ShlexParser())

Add a shlex parser command parser to a supported command.

Example
@tanjun.with_argument("arg", converters=int)
@tanjun.with_parser
@tanjun.as_message_command("hi")
async def hi(ctx: tanjun.MessageContext, arg: int) -> None:
    ...
Parameters
Returns
Raises
  • ValueError: If the command already has a parser set.
View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Collection of utility functions used within Tanjun."""
from __future__ import annotations

__all__: list[str] = [
    "gather_checks",
    "ALL_PERMISSIONS",
    "CastedView",
    "DM_PERMISSIONS",
    "calculate_everyone_permissions",
    "calculate_permissions",
    "fetch_everyone_permissions",
    "fetch_permissions",
    "match_prefix_names",
]

import asyncio
import typing
from collections import abc as collections

import hikari

from . import errors
from . import injecting
from .dependencies import async_cache

if typing.TYPE_CHECKING:
    from . import abc
    from . import checks


_KeyT = typing.TypeVar("_KeyT")
_ValueT = typing.TypeVar("_ValueT")
_OtherValueT = typing.TypeVar("_OtherValueT")


async def gather_checks(ctx: abc.Context, checks_: collections.Iterable[checks.InjectableCheck], /) -> bool:
    """Gather a collection of checks.

    Parameters
    ----------
    ctx : tanjun.abc.Context
        The context to check.
    checks : collections.abc.Iterable[tanjun.injecting.InjectableCheck]
        An iterable of injectable checks.

    Returns
    -------
    bool
        Whether all the checks passed or not.
    """
    try:
        await asyncio.gather(*(check(ctx) for check in checks_))
        # InjectableCheck will raise FailedCheck if a false is received so if
        # we get this far then it's True.
        return True

    except errors.FailedCheck:
        return False


def match_prefix_names(content: str, names: collections.Iterable[str], /) -> typing.Optional[str]:
    """Search for a matching name in a string.

    Parameters
    ----------
    content : str
        The string to match names against.
    names : collections.abc.Iterable[str]
        The names to search for.

    Returns
    -------
    typing.Optional[str]
        The name that matched or None if no name matched.
    """
    for name in names:
        # Here we enforce that a name must either be at the end of content or be followed by a space. This helps
        # avoid issues with ambiguous naming where a command with the names "name" and "names" may sometimes hit
        # the former before the latter when triggered with the latter, leading to the command potentially being
        # inconsistently parsed.
        if content == name or content.startswith(name) and content[len(name)] == " ":
            return name


ALL_PERMISSIONS: typing.Final[hikari.Permissions] = hikari.Permissions.all_permissions()
"""All of all the known permissions based on the linked version of Hikari."""

DM_PERMISSIONS: typing.Final[hikari.Permissions] = (
    hikari.Permissions.ADD_REACTIONS
    | hikari.Permissions.VIEW_CHANNEL
    | hikari.Permissions.SEND_MESSAGES
    | hikari.Permissions.EMBED_LINKS
    | hikari.Permissions.ATTACH_FILES
    | hikari.Permissions.READ_MESSAGE_HISTORY
    | hikari.Permissions.USE_EXTERNAL_EMOJIS
    | hikari.Permissions.USE_EXTERNAL_STICKERS
    | hikari.Permissions.USE_APPLICATION_COMMANDS
)
"""Bitfield of the permissions which are accessibly within DM channels."""


def _calculate_channel_overwrites(
    channel: hikari.GuildChannel, member: hikari.Member, permissions: hikari.Permissions
) -> hikari.Permissions:
    if everyone_overwrite := channel.permission_overwrites.get(member.guild_id):
        permissions &= ~everyone_overwrite.deny
        permissions |= everyone_overwrite.allow

    deny = hikari.Permissions.NONE
    allow = hikari.Permissions.NONE

    for overwrite in filter(None, map(channel.permission_overwrites.get, member.role_ids)):
        deny |= overwrite.deny
        allow |= overwrite.allow

    permissions &= ~deny
    permissions |= allow

    if member_overwrite := channel.permission_overwrites.get(member.user.id):
        permissions &= ~member_overwrite.deny
        permissions |= member_overwrite.allow

    return permissions


def _calculate_role_permissions(
    roles: collections.Mapping[hikari.Snowflake, hikari.Role], member: hikari.Member
) -> hikari.Permissions:
    permissions = roles[member.guild_id].permissions

    for role in map(roles.get, member.role_ids):
        if role and role.id != member.guild_id:
            permissions |= role.permissions

    return permissions


# TODO: implicitly handle more special cases?
def calculate_permissions(
    member: hikari.Member,
    guild: hikari.Guild,
    roles: collections.Mapping[hikari.Snowflake, hikari.Role],
    *,
    channel: typing.Optional[hikari.GuildChannel] = None,
) -> hikari.Permissions:
    """Calculate the permissions a member has within a guild.

    Parameters
    ----------
    member : hikari.guilds.Member
        Object of the member to calculate the permissions for.
    guild : hikari.guilds.Guild
        Object of the guild to calculate their permissions within.
    roles : collections.abc.Mapping[hikari.snowflakes.Snowflake, hikari.guilds.Role]
        Mapping of snowflake IDs to objects of the roles within the target
        guild.

    Other Parameters
    ----------------
    channel : typing.Optional[hikari.channels.GuildChannel]
        Object of the channel to calculate the member's permissions in.

        If this is left as `None` then this will just calculate their
        permissions on a guild level.

    Returns
    -------
    hikari.permissions.Permission
        Value of the member's permissions either within the guild or specified
        guild channel.
    """
    if member.guild_id != guild.id:
        raise ValueError("Member object isn't from the provided guild")

    # Guild owners are implicitly admins.
    if guild.owner_id == member.user.id:
        return ALL_PERMISSIONS

    # Admin permission overrides all overwrites and is only applicable to roles.
    if (permissions := _calculate_role_permissions(roles, member)) & permissions.ADMINISTRATOR:
        return ALL_PERMISSIONS

    if not channel:
        return permissions

    return _calculate_channel_overwrites(channel, member, permissions)


async def _fetch_channel(
    client: abc.Client, channel: hikari.SnowflakeishOr[hikari.PartialChannel]
) -> hikari.GuildChannel:
    # TODO: upgrade injecting stuff to the standard interface
    assert isinstance(client, injecting.InjectorClient)

    if isinstance(channel, hikari.GuildChannel):
        return channel

    channel_id = hikari.Snowflake(channel)
    if client.cache and (found_channel_ := client.cache.get_guild_channel(channel_id)):
        return found_channel_

    if channel_cache := client.get_type_dependency(_ChannelCacheT):
        try:
            return await channel_cache.get(channel_id)

        except async_cache.EntryNotFound:
            raise

        except async_cache.CacheMissError:
            pass

    found_channel = await client.rest.fetch_channel(channel_id)
    assert isinstance(found_channel, hikari.GuildChannel), "Cannot perform operation on a DM channel."
    return found_channel


_ChannelCacheT = async_cache.SfCache[hikari.GuildChannel]
_GuildCacheT = async_cache.SfCache[hikari.Guild]
_RoleCacheT = async_cache.SfCache[hikari.Role]
_GuldRoleCacheT = async_cache.SfGuildBound[hikari.Role]


async def fetch_permissions(
    client: abc.Client,
    member: hikari.Member,
    /,
    *,
    channel: typing.Optional[hikari.SnowflakeishOr[hikari.PartialChannel]] = None,
) -> hikari.Permissions:
    """Calculate the permissions a member has within a guild.

    .. note::
        This callback will fallback to REST requests if cache lookups fail or
        are not possible.

    Parameters
    ----------
    client : tanjun.abc.Client
        The Tanjun client to use for lookups.
    member : hikari.guilds.Member
        The object of the member to calculate the permissions for.

    Other Parameters
    ----------------
    channel : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.channels.GuildChannel]]
        The object of ID of the channel to get their permissions in.
        If left as `None` then this will return their base guild
        permissions.

    Returns
    -------
    hikari.permissions.Permissions
        The calculated permissions.
    """
    # TODO: upgrade injecting stuff to the standard interface
    assert isinstance(client, injecting.InjectorClient)

    # The ordering of how this adds and removes permissions does matter.
    # For more information see https://discord.com/developers/docs/topics/permissions#permission-hierarchy.
    guild: typing.Optional[hikari.Guild]
    roles: typing.Optional[collections.Mapping[hikari.Snowflake, hikari.Role]] = None
    guild = client.cache.get_guild(member.guild_id) if client.cache else None
    if not guild and (guild_cache := client.get_type_dependency(_GuildCacheT)):
        try:
            guild = await guild_cache.get(member.guild_id)

        except async_cache.EntryNotFound:
            raise

        except async_cache.CacheMissError:
            pass

    if not guild:
        guild = await client.rest.fetch_guild(member.guild_id)
        roles = guild.roles

    # Guild owners are implicitly admins.
    if guild.owner_id == member.user.id:
        return ALL_PERMISSIONS

    roles = roles or client.cache and client.cache.get_roles_view_for_guild(member.guild_id)
    if not roles and (role_cache := client.get_type_dependency(_GuldRoleCacheT)):
        roles = {role.id: role for role in await role_cache.iter_for_guild(member.guild_id)}

    if not roles:
        raw_roles = await client.rest.fetch_roles(member.guild_id)
        roles = {role.id: role for role in raw_roles}

    # Admin permission overrides all overwrites and is only applicable to roles.
    if (permissions := _calculate_role_permissions(roles, member)) & permissions.ADMINISTRATOR:
        return ALL_PERMISSIONS

    if not channel:
        return permissions

    channel = await _fetch_channel(client, channel)
    if channel.guild_id != guild.id:
        raise ValueError("Channel doesn't match up with the member's guild")

    return _calculate_channel_overwrites(channel, member, permissions)


def calculate_everyone_permissions(
    everyone_role: hikari.Role,
    /,
    *,
    channel: typing.Optional[hikari.GuildChannel] = None,
) -> hikari.Permissions:
    """Calculate a guild's default permissions within the guild or for a specific channel.

    Parameters
    ----------
    everyone_role : hikari.guilds.Role
        The guild's default @everyone role.

    Other Parameters
    ----------------
    channel : typing.Optional[hikari.channels.GuildChannel]
        The channel to calculate the permissions for.

        If this is left as `None` then this will just calculate the default
        permissions on a guild level.

    Returns
    -------
    hikari.permissions.Permissions
        The calculated permissions.
    """
    # The ordering of how this adds and removes permissions does matter.
    # For more information see https://discord.com/developers/docs/topics/permissions#permission-hierarchy.
    permissions = everyone_role.permissions
    # Admin permission overrides all overwrites and is only applicable to roles.
    if permissions & permissions.ADMINISTRATOR:
        return ALL_PERMISSIONS

    if not channel:
        return permissions

    if everyone_overwrite := channel.permission_overwrites.get(everyone_role.guild_id):
        permissions &= ~everyone_overwrite.deny
        permissions |= everyone_overwrite.allow

    return permissions


async def fetch_everyone_permissions(
    client: abc.Client,
    guild_id: hikari.Snowflake,
    /,
    *,
    channel: typing.Optional[hikari.SnowflakeishOr[hikari.PartialChannel]] = None,
) -> hikari.Permissions:
    """Calculate the permissions a guild's default @everyone role has within a guild or for a specific channel.

    .. note::
        This callback will fallback to REST requests if cache lookups fail or
        are not possible.

    Parameters
    ----------
    client : tanjun.abc.Client
        The Tanjun client to use for lookups.
    guild_id : hikari.snowflakes.Snowflake
        ID of the guild to calculate the default permissions for.

    Other Parameters
    ----------------
    channel : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]]
        The channel to calculate the permissions for.

        If this is left as `None` then this will just calculate the default
        permissions on a guild level.


    Returns
    -------
    hikari.permissions.Permissions
        The calculated permissions.
    """
    # TODO: upgrade injecting stuff to the standard interface
    assert isinstance(client, injecting.InjectorClient)
    # The ordering of how this adds and removes permissions does matter.
    # For more information see https://discord.com/developers/docs/topics/permissions#permission-hierarchy.
    role = client.cache.get_role(guild_id) if client.cache else None
    if not role and (role_cache := client.get_type_dependency(_RoleCacheT)):
        try:
            role = await role_cache.get(guild_id)

        except async_cache.EntryNotFound:
            raise

        except async_cache.CacheMissError:
            pass

    if not role:
        for role in await client.rest.fetch_roles(guild_id):
            if role.id == guild_id:
                break

        else:
            raise RuntimeError("Failed to find guild's @everyone role")

    permissions = role.permissions
    # Admin permission overrides all overwrites and is only applicable to roles.
    if permissions & permissions.ADMINISTRATOR:
        return ALL_PERMISSIONS

    if not channel:
        return permissions

    channel = await _fetch_channel(client, channel)
    if everyone_overwrite := channel.permission_overwrites.get(guild_id):
        permissions &= ~everyone_overwrite.deny
        permissions |= everyone_overwrite.allow

    return permissions


class CastedView(collections.Mapping[_KeyT, _OtherValueT], typing.Generic[_KeyT, _ValueT, _OtherValueT]):
    __slots__ = ("_buffer", "_cast", "_raw_data")

    def __init__(self, raw_data: dict[_KeyT, _ValueT], cast: collections.Callable[[_ValueT], _OtherValueT]) -> None:
        self._buffer: dict[_KeyT, _OtherValueT] = {}
        self._cast = cast
        self._raw_data = raw_data

    def __getitem__(self, key: _KeyT, /) -> _OtherValueT:
        try:
            return self._buffer[key]

        except KeyError:
            pass

        entry = self._raw_data[key]
        result = self._cast(entry)
        self._buffer[key] = result
        return result

    def __iter__(self) -> collections.Iterator[_KeyT]:
        return iter(self._raw_data)

    def __len__(self) -> int:
        return len(self._raw_data)

Collection of utility functions used within Tanjun.

View Source
# -*- coding: utf-8 -*-
# cython: language_level=3
# BSD 3-Clause License
#
# Copyright (c) 2020-2022, Faster Speeding
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Interface and interval implementation for a Tanjun based callback scheduler."""
from __future__ import annotations

__all__: list[str] = ["AbstractSchedule", "as_interval", "IntervalSchedule"]

import abc
import asyncio
import copy
import datetime
import typing

from . import components
from . import injecting

if typing.TYPE_CHECKING:
    from collections import abc as collections

    from . import abc as tanjun_abc

    _CallbackSig = collections.Callable[..., collections.Awaitable[None]]
    _OtherCallbackT = typing.TypeVar("_OtherCallbackT", bound="_CallbackSig")
    _IntervalScheduleT = typing.TypeVar("_IntervalScheduleT", bound="IntervalSchedule[typing.Any]")
    _T = typing.TypeVar("_T")

_CallbackSigT = typing.TypeVar("_CallbackSigT", bound="_CallbackSig")


class AbstractSchedule(abc.ABC):
    """Abstract callback schedule class."""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def callback(self) -> _CallbackSig:
        """Return the callback attached to the schedule.

        This will be an asynchronous function which takes zero positional
        arguments, returns `None` and may be relying on dependency injection.
        """

    @property
    @abc.abstractmethod
    def is_alive(self) -> bool:
        """Whether the schedule is alive."""

    @property
    @abc.abstractmethod
    def iteration_count(self) -> int:
        """Return the number of times this schedule has run.

        This increments after a call regardless of if it failed.
        """

    @abc.abstractmethod
    def copy(self: _T) -> _T:
        """Copy the schedule.

        Returns
        -------
        Self
            The copied schedule.

        Raises
        ------
        RuntimeError
            If the schedule is active.
        """

    @abc.abstractmethod
    def start(
        self, client: injecting.InjectorClient, /, *, loop: typing.Optional[asyncio.AbstractEventLoop] = None
    ) -> None:
        """Start the schedule.

        Parameters
        ----------
        tanjun.injecting.InjectorClient
            The injector client calls should be resolved with.

        Other Parameters
        ----------------
        loop : typing.Optional[asyncio.AbstractEventLoop]
            The event loop to use. If not provided, the current event loop will
            be used.

        Raises
        ------
        RuntimeError
            If the scheduled callback is already running.
            If the current or provided event loop isn't running.
        """

    @abc.abstractmethod
    def stop(self) -> None:
        """Stop the schedule.

        Raises
        ------
        RuntimeError
            If the scheduled callback isn't running.
        """


@typing.runtime_checkable
class _ComponentProto(typing.Protocol):
    def add_schedule(self, schedule: AbstractSchedule, /) -> typing.Any:
        raise NotImplementedError


def as_interval(
    interval: typing.Union[int, float, datetime.timedelta],
    /,
    *,
    fatal_exceptions: collections.Sequence[type[Exception]] = (),
    ignored_exceptions: collections.Sequence[type[Exception]] = (),
    max_runs: typing.Optional[int] = None,
) -> collections.Callable[[_CallbackSigT], IntervalSchedule[_CallbackSigT]]:
    """Decorator to create an schedule.

    Parameters
    ----------
    interval : typing.Union[int, float, datetime.timedelta]
        The callback for the schedule.

        This should be an asynchronous function which takes no positional
        arguments, returns `None` and may use dependency injection.

    Other Parameters
    ----------------
    fatal_exceptions : collections.abc.Sequence[type[Exception]]
        A sequence of exceptions that will cause the schedule to stop if raised
        by the callback, start callback or stop callback.

        Defaults to no exceptions.
    ignored_exceptions : collections.abc.Sequence[type[Exception]]
        A sequence of exceptions that should be ignored if raised by the
        callback, start callback or stop callback.

        Defaults to no exceptions.
    max_runs : typing.Optional[int]
        The maximum amount of times the schedule runs. Defaults to no maximum.

    Returns
    -------
    collections.Callable[[_CallbackSigT], tanjun.scheduling.IntervalSchedule[_CallbackSigT]]
        The decorator used to create the schedule.
    """
    return lambda callback: IntervalSchedule(
        callback,
        interval,
        fatal_exceptions=fatal_exceptions,
        ignored_exceptions=ignored_exceptions,
        max_runs=max_runs,
    )


class IntervalSchedule(typing.Generic[_CallbackSigT], components.AbstractComponentLoader, AbstractSchedule):
    """A callback schedule with an interval between calls."""

    __slots__ = (
        "_callback",
        "_fatal_exceptions",
        "_ignored_exceptions",
        "_interval",
        "_iteration_count",
        "_max_runs",
        "_stop_callback",
        "_start_callback",
        "_task",
    )

    def __init__(
        self,
        callback: _CallbackSigT,
        interval: typing.Union[datetime.timedelta, int, float],
        /,
        *,
        fatal_exceptions: collections.Sequence[type[Exception]] = (),
        ignored_exceptions: collections.Sequence[type[Exception]] = (),
        max_runs: typing.Optional[int] = None,
    ) -> None:
        """Initialise an interval schedule.

        Parameters
        ----------
        callback : collections.abc.Callable[...,  collections.abc.Awaitable[None]]
            The callback for the schedule.

            This should be an asynchronous function which takes no positional
            arguments, returns `None` and may use dependency injection.
        interval : typing.Union[datetime.timedelta, int, float]
            The interval between calls. Passed as a timedelta, or a number of seconds.

        Other Parameters
        ----------------
        fatal_exceptions : collections.abc.Sequence[type[Exception]]
            A sequence of exceptions that will cause the schedule to stop if raised
            by the callback, start callback or stop callback.

            Defaults to no exceptions.
        ignored_exceptions : collections.abc.Sequence[type[Exception]]
            A sequence of exceptions that should be ignored if raised by the
            callback, start callback or stop callback.

            Defaults to no exceptions.
        max_runs : typing.Optional[int]
            The maximum amount of times the schedule runs. Defaults to no maximum.
        """
        if isinstance(interval, datetime.timedelta):
            self._interval: datetime.timedelta = interval
        else:
            self._interval: datetime.timedelta = datetime.timedelta(seconds=interval)

        self._callback = injecting.CallbackDescriptor[None](callback)
        self._fatal_exceptions = tuple(fatal_exceptions)
        self._ignored_exceptions = tuple(ignored_exceptions)
        self._iteration_count: int = 0
        self._max_runs = max_runs
        self._stop_callback: typing.Optional[injecting.CallbackDescriptor[None]] = None
        self._start_callback: typing.Optional[injecting.CallbackDescriptor[None]] = None
        self._task: typing.Optional[asyncio.Task[None]] = None

    @property
    def callback(self) -> _CallbackSigT:
        # <<inherited docstring from IntervalSchedule>>.
        return typing.cast(_CallbackSigT, self._callback.callback)

    @property
    def interval(self) -> datetime.timedelta:
        """The interval between scheduled callback calls."""
        return self._interval

    @property
    def is_alive(self) -> bool:
        # <<inherited docstring from IntervalSchedule>>.
        return self._task is not None

    @property
    def iteration_count(self) -> int:
        # <<inherited docstring from IntervalSchedule>>.
        return self._iteration_count

    if typing.TYPE_CHECKING:
        __call__: _CallbackSigT

    else:

        async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
            await self._callback.callback(*args, **kwargs)

    def copy(self: _IntervalScheduleT) -> _IntervalScheduleT:
        # <<inherited docstring from IntervalSchedule>>.
        if self._task:
            raise RuntimeError("Cannot copy an active schedule")

        return copy.copy(self)

    def load_into_component(self, component: tanjun_abc.Component, /) -> None:
        # <<inherited docstring from tanjun.components.AbstractComponentLoader>>.
        if isinstance(component, _ComponentProto):
            component.add_schedule(self)

    def set_start_callback(self: _IntervalScheduleT, callback: _CallbackSig, /) -> _IntervalScheduleT:
        """Set the callback executed before the schedule starts to run.

        Parameters
        ----------
        callback : CallbackSig
            The callback to set.

        Returns
        -------
        Self
            The schedule instance to enable chained calls.
        """
        self._start_callback = injecting.CallbackDescriptor(callback)
        return self

    def set_stop_callback(self: _IntervalScheduleT, callback: _CallbackSig, /) -> _IntervalScheduleT:
        """Set the callback executed after the schedule is finished.

        Parameters
        ----------
        callback : collections.abc.Callable[...,  collections.abc.Awaitable[None]]
            The callback to set.

        Returns
        -------
        Self
            The schedule instance to enable chained calls.
        """
        self._stop_callback = injecting.CallbackDescriptor(callback)
        return self

    async def _execute(self, client: injecting.InjectorClient, /) -> None:
        try:
            await self._callback.resolve(injecting.BasicInjectionContext(client))

        except self._fatal_exceptions:
            self.stop()
            raise

        except self._ignored_exceptions:
            pass

    async def _loop(self, client: injecting.InjectorClient, /) -> None:
        event_loop = asyncio.get_running_loop()
        try:
            if self._start_callback:
                try:
                    await self._start_callback.resolve(injecting.BasicInjectionContext(client))

                except self._ignored_exceptions:
                    pass

            while not self._max_runs or self._iteration_count < self._max_runs:
                self._iteration_count += 1
                event_loop.create_task(self._execute(client))
                await asyncio.sleep(self._interval.total_seconds())

        finally:
            self._task = None
            if self._stop_callback:
                try:
                    await self._stop_callback.resolve(injecting.BasicInjectionContext(client))

                except self._ignored_exceptions:
                    pass

    def start(
        self, client: injecting.InjectorClient, /, *, loop: typing.Optional[asyncio.AbstractEventLoop] = None
    ) -> None:
        # <<inherited docstring from IntervalSchedule>>.
        if self._task:
            raise RuntimeError("Cannot start an active schedule")

        loop = loop or asyncio.get_running_loop()

        if not loop.is_running():
            raise RuntimeError("Event loop is not running")

        self._task = loop.create_task(self._loop(client))

    def stop(self) -> None:
        # <<inherited docstring from IntervalSchedule>>.
        if not self._task:
            raise RuntimeError("Schedule is not running")

        self._task.cancel()
        self._task = None

    def with_start_callback(self, callback: _OtherCallbackT, /) -> _OtherCallbackT:
        """Set the callback executed before the schedule is finished/stopped.

        Parameters
        ----------
        callback : collections.abc.Callable[...,  collections.abc.Awaitable[None]]
            The callback to set.

        Returns
        -------
        collections.abc.Callable[...,  collections.abc.Awaitable[None]]
            The added callback.

        Examples
        --------
        ```py
        @component.with_schedule()
        @tanjun.as_interval(1, max_runs=20)
        async def interval():
            global counter
            counter += 1
            print(f"Run #{counter}")

        @interval.with_start_callback
        async def pre():
            print("pre callback")
        ```
        """
        self.set_start_callback(callback)
        return callback

    def with_stop_callback(self, callback: _OtherCallbackT, /) -> _OtherCallbackT:
        """Set the callback executed after the schedule is finished.

        Parameters
        ----------
        callback : collections.abc.Callable[...,  collections.abc.Awaitable[None]]
            The callback to set.

        Returns
        -------
        collections.abc.Callable[...,  collections.abc.Awaitable[None]]
            The added callback.

        Examples
        --------
        ```py
        @component.with_schedule()
        @tanjun.as_interval(1, max_runs=20)
        async def interval():
            global counter
            counter += 1
            print(f"Run #{counter}")


        @interval.with_stop_callback
        async def post():
            print("pre callback")
        ```
        """
        self.set_stop_callback(callback)
        return callback

    def set_ignored_exceptions(self: _IntervalScheduleT, *exceptions: type[Exception]) -> _IntervalScheduleT:
        """Set the exceptions that a schedule will ignore.

        If any of these exceptions are encountered, there will be nothing printed to console.

        Parameters
        ----------
        *exceptions : type[Exception]
            Types of the exceptions to ignore.

        Returns
        -------
        Self
            The schedule object to enable chained calls.
        """
        self._ignored_exceptions = exceptions
        return self

    def set_fatal_exceptions(self: _IntervalScheduleT, *exceptions: type[Exception]) -> _IntervalScheduleT:
        """Set the exceptions that will stop a schedule.

        If any of these exceptions are encountered, the task will stop.

        Parameters
        ----------
        *exceptions : type[Exception]
            Types of the exceptions to stop the task on.

        Returns
        -------
        Self
            The schedule object to enable chianed calls.
        """
        self._fatal_exceptions = exceptions
        return self

Interface and interval implementation for a Tanjun based callback scheduler.

#   def as_interval( interval: Union[int, float, datetime.timedelta], /, *, fatal_exceptions: collections.abc.Sequence[type[Exception]] = (), ignored_exceptions: collections.abc.Sequence[type[Exception]] = (), max_runs: Optional[int] = None ) -> collections.abc.Callable[[~_CallbackSigT], tanjun.schedules.IntervalSchedule[~_CallbackSigT]]:
View Source
def as_interval(
    interval: typing.Union[int, float, datetime.timedelta],
    /,
    *,
    fatal_exceptions: collections.Sequence[type[Exception]] = (),
    ignored_exceptions: collections.Sequence[type[Exception]] = (),
    max_runs: typing.Optional[int] = None,
) -> collections.Callable[[_CallbackSigT], IntervalSchedule[_CallbackSigT]]:
    """Decorator to create an schedule.

    Parameters
    ----------
    interval : typing.Union[int, float, datetime.timedelta]
        The callback for the schedule.

        This should be an asynchronous function which takes no positional
        arguments, returns `None` and may use dependency injection.

    Other Parameters
    ----------------
    fatal_exceptions : collections.abc.Sequence[type[Exception]]
        A sequence of exceptions that will cause the schedule to stop if raised
        by the callback, start callback or stop callback.

        Defaults to no exceptions.
    ignored_exceptions : collections.abc.Sequence[type[Exception]]
        A sequence of exceptions that should be ignored if raised by the
        callback, start callback or stop callback.

        Defaults to no exceptions.
    max_runs : typing.Optional[int]
        The maximum amount of times the schedule runs. Defaults to no maximum.

    Returns
    -------
    collections.Callable[[_CallbackSigT], tanjun.scheduling.IntervalSchedule[_CallbackSigT]]
        The decorator used to create the schedule.
    """
    return lambda callback: IntervalSchedule(
        callback,
        interval,
        fatal_exceptions=fatal_exceptions,
        ignored_exceptions=ignored_exceptions,
        max_runs=max_runs,
    )

Decorator to create an schedule.

Parameters
  • interval (typing.Union[int, float, datetime.timedelta]): The callback for the schedule.

    This should be an asynchronous function which takes no positional arguments, returns None and may use dependency injection.

Other Parameters
  • fatal_exceptions (collections.abc.Sequence[type[Exception]]): A sequence of exceptions that will cause the schedule to stop if raised by the callback, start callback or stop callback.

    Defaults to no exceptions.

  • ignored_exceptions (collections.abc.Sequence[type[Exception]]): A sequence of exceptions that should be ignored if raised by the callback, start callback or stop callback.

    Defaults to no exceptions.

  • max_runs (typing.Optional[int]): The maximum amount of times the schedule runs. Defaults to no maximum.
Returns